strongly_typed 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/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/.yardopts +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +114 -0
- data/Rakefile +6 -0
- data/lib/strongly_typed/attributes.rb +94 -0
- data/lib/strongly_typed/coercible.rb +64 -0
- data/lib/strongly_typed/model.rb +99 -0
- data/lib/strongly_typed/version.rb +3 -0
- data/lib/strongly_typed.rb +6 -0
- data/spec/coercible_spec.rb +65 -0
- data/spec/model_spec.rb +95 -0
- data/spec/spec_helper.rb +18 -0
- data/strongly_typed.gemspec +41 -0
- metadata +176 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Leo Gallucci
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# StronglyTyped
|
2
|
+
|
3
|
+
[](https://travis-ci.org/elgalu/strongly_typed)
|
4
|
+
[](https://gemnasium.com/elgalu/strongly_typed)
|
5
|
+
[](https://codeclimate.com/github/elgalu/strongly_typed)
|
6
|
+
|
7
|
+
This gem provides similar functionality as ruby core [Struct][] but instead of [inheritance][] i used [mixins][] and even wrote [something][blog on mixins] about my reasons to do so.
|
8
|
+
|
9
|
+
Same as [Struct][], it is a convenient way to bundle a number of attributes together using accessor methods with added features like basic type validation and type conversions when [possible][specs on conversions].
|
10
|
+
|
11
|
+
## Similar Tools
|
12
|
+
|
13
|
+
From ruby core you have [Struct][] and from stdlib [OpenStruct][].
|
14
|
+
|
15
|
+
Check [virtus][] if you are looking for a full featured attributes settings for your Ruby Objects that requires complex automatic coercions among other things.
|
16
|
+
|
17
|
+
If you are looking for a nestable, coercible, Hash-like data structure take a look at [structure][] gem.
|
18
|
+
|
19
|
+
## Reasons
|
20
|
+
|
21
|
+
I took some ideas from others gems like [virtus][] and [structure][] but since none of them provided exactly what i needed then decided to create this gem for my sample gem project [dolarblue][] and the blog [post][blog on dolarblue] i wrote about it.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
gem 'strongly_typed'
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install strongly_typed
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
Include `StronglyTyped::Model` on you ruby objects then call `attribute()` to define them.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'strongly_typed'
|
43
|
+
|
44
|
+
class Person
|
45
|
+
include StronglyTyped::Model
|
46
|
+
|
47
|
+
attribute :id, Integer
|
48
|
+
attribute :slug, String
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Instance initialization with a hash, a.k.a [named parameters][]
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
leo = Person.new(id: 1, slug: 'elgalu')
|
56
|
+
#=> #<Person:0x00c98 @id=1, @slug="elgalu">
|
57
|
+
leo.id #=> 1
|
58
|
+
leo.slug #=> "elgalu"
|
59
|
+
```
|
60
|
+
|
61
|
+
Can also trust in the parameters order
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
leo = Person.new(1, 'elgalu')
|
65
|
+
#=> #<Person:0x00c99 @id=1, @slug="elgalu">
|
66
|
+
leo.id #=> 1
|
67
|
+
leo.slug #=> "elgalu"
|
68
|
+
```
|
69
|
+
|
70
|
+
Hash Order doesn't matter
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
ana = Person.new(slug: 'anapau', id: 2)
|
74
|
+
#=> #<Person:0x00d33 @id=2, @slug="anapau">
|
75
|
+
ana.id #=> 2
|
76
|
+
ana.slug #=> "anapau"
|
77
|
+
```
|
78
|
+
|
79
|
+
TypeError is raised when improper type
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
ted = Person.new(id: nil, slug: 'teddy')
|
83
|
+
#=> TypeError: can't convert nil into Integer
|
84
|
+
```
|
85
|
+
|
86
|
+
Check the [full API here][]
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork it
|
91
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
94
|
+
5. Create new Pull Request
|
95
|
+
|
96
|
+
### TODO
|
97
|
+
+ Improve generic TypeError to also show attribute name, expected type and value received instead
|
98
|
+
+ On coercible.rb require gem 'double' to avoid requiring 'date' when user doesn't need that
|
99
|
+
+ Add :default => 'value' to attribute() to support defaults
|
100
|
+
+ Add :required => true/false to attribute() so an ArgumentError is raised when not provided
|
101
|
+
|
102
|
+
|
103
|
+
[strongly_typed]: https://github.com/elgalu/strongly_typed
|
104
|
+
[full API here]: http://rubydoc.info/gems/strongly_typed/frames/file/README.md
|
105
|
+
[specs on conversions]: https://github.com/elgalu/strongly_typed/blob/master/spec/coercible_spec.rb
|
106
|
+
[blog on dolarblue]: http://elgalu.github.com/2013/tools-for-creating-your-first-ruby-gem-dolarblue/
|
107
|
+
[blog on mixins]: http://elgalu.github.com/2013/when-to-use-ruby-inheritance-versus-mixins/
|
108
|
+
[named parameters]: http://en.wikipedia.org/wiki/Named_parameter
|
109
|
+
[Struct]: http://www.ruby-doc.org/core-1.9.3/Struct.html
|
110
|
+
[OpenStruct]: http://ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html
|
111
|
+
[structure]: https://github.com/hakanensari/structure
|
112
|
+
[virtus]: https://github.com/solnic/virtus
|
113
|
+
[inheritance]: http://en.wikipedia.org/wiki/Inheritance_(computer_science)
|
114
|
+
[mixins]: http://en.wikipedia.org/wiki/Mixin
|
data/Rakefile
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module StronglyTyped
|
2
|
+
module Attributes
|
3
|
+
|
4
|
+
# Create attribute accesors for the included class
|
5
|
+
# Also validations and coercions for the type specified
|
6
|
+
#
|
7
|
+
# @param [Symbol] name the accessor name
|
8
|
+
# @param [Class] type the class type to use for validations and coercions
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class Person
|
12
|
+
# include StronglyTyped::Model
|
13
|
+
#
|
14
|
+
# attribute :id, Integer
|
15
|
+
# attribute :slug, String
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Person.new(id: 1, slug: 'elgalu')
|
19
|
+
# #=> #<Person:0x00c98 @id=1, @slug="elgalu">
|
20
|
+
# leo.id #=> 1
|
21
|
+
# leo.slug #=> "elgalu"
|
22
|
+
def attribute(name, type=Object)
|
23
|
+
name = name.to_sym #normalize
|
24
|
+
|
25
|
+
raise NameError, "attribute `#{name}` already created" if members.include?(name)
|
26
|
+
raise TypeError, "second argument, type, must be a Class but got `#{type.inspect}` insted" unless type.is_a?(Class)
|
27
|
+
raise TypeError, "directly converting to Bignum is not supported, use Integer instead" if type == Bignum
|
28
|
+
|
29
|
+
new_attribute(name, type)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Memoized hash storing keys for names & values for types pairs
|
33
|
+
#
|
34
|
+
# @return [Hash] attributes
|
35
|
+
def attributes
|
36
|
+
@attributes ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the attribute names created with attribute()
|
40
|
+
#
|
41
|
+
# @return [Array<Symbol>] all the attribute names
|
42
|
+
def members
|
43
|
+
attributes.keys
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Add new attribute for the tiny object modeled
|
49
|
+
#
|
50
|
+
# @param [Symbol] name the attribute name
|
51
|
+
# @param [Class] type the class type
|
52
|
+
#
|
53
|
+
# @private
|
54
|
+
def new_attribute(name, type)
|
55
|
+
attributes[name] = type
|
56
|
+
define_attr_reader(name)
|
57
|
+
define_attr_writer(name, type)
|
58
|
+
name
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define attr_reader method for the new attribute
|
62
|
+
#
|
63
|
+
# @param [Symbol] name the attribute name
|
64
|
+
#
|
65
|
+
# @private
|
66
|
+
def define_attr_reader(name)
|
67
|
+
define_method(name) do
|
68
|
+
instance_variable_get("@#{name}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Define attr_writer method for the new attribute
|
73
|
+
# with the added feature of validations and coercions.
|
74
|
+
#
|
75
|
+
# @param [Symbol] name the attribute name
|
76
|
+
# @param [Class] type the class type
|
77
|
+
#
|
78
|
+
# @raise [TypeError] if unable to coerce the value
|
79
|
+
#
|
80
|
+
# @private
|
81
|
+
def define_attr_writer(name, type)
|
82
|
+
define_method("#{name}=") do |value|
|
83
|
+
unless value.kind_of?(type)
|
84
|
+
value = coerce(value, to: type)
|
85
|
+
unless value.kind_of?(type)
|
86
|
+
raise TypeError, "Attribute `#{name}` only accepts `#{type}` but got `#{value}`:`#{value.class}` instead"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
instance_variable_set("@#{name}", value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'boolean_class'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module StronglyTyped
|
5
|
+
module Coercible
|
6
|
+
LOCAL_OFFSET = Float(Time.now.gmt_offset) / Float(3600)
|
7
|
+
|
8
|
+
# Coerce (convert) a value to some specified type
|
9
|
+
#
|
10
|
+
# @param [Object] value the value to coerce
|
11
|
+
# @param [Hash] opts the conversion options
|
12
|
+
# @option opts [Class, Module] :to the type to convert to
|
13
|
+
#
|
14
|
+
# @return [Object] the converted value into the specified type
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# include StronglyTyped::Coercible
|
18
|
+
#
|
19
|
+
# coerce 100, to: Float #=> 100.0
|
20
|
+
# coerce 100 #=> ArgumentError: Needs option :to => Class/Module
|
21
|
+
# coerce 100, to: String #=> "100"
|
22
|
+
# coerce 100, to: Boolean #=> true
|
23
|
+
# coerce 100, to: Symbol #=> TypeError: can't convert `100:Fixnum` to `Symbol`
|
24
|
+
#
|
25
|
+
# @raise [ArgumentError] if :to => Class option was not provided correctly
|
26
|
+
# @raise [TypeError] if unable to perform the coersion
|
27
|
+
def coerce(value, opts={})
|
28
|
+
raise ArgumentError, "Needs option :to => Class/Module" unless opts.has_key?(:to) && ( opts[:to].is_a?(Class) || opts[:to].is_a?(Module) )
|
29
|
+
type = opts[:to]
|
30
|
+
|
31
|
+
case
|
32
|
+
# Direct conversions
|
33
|
+
when type <= String then String(value)
|
34
|
+
when type <= Boolean then Boolean(value)
|
35
|
+
when type == Bignum then raise TypeError, "directly converting to Bignum is not supported, use Integer instead"
|
36
|
+
when type <= Integer then Integer(value)
|
37
|
+
when type <= Float then Float(value)
|
38
|
+
when type <= Rational then Rational(value)
|
39
|
+
when type <= Complex then Complex(value)
|
40
|
+
# Symbol
|
41
|
+
when type <= Symbol && value.respond_to?(:to_sym)
|
42
|
+
value.to_sym
|
43
|
+
# Dates and Times
|
44
|
+
when type <= Time && value.is_a?(Numeric)
|
45
|
+
Time.at(value)
|
46
|
+
when type <= Time && value.is_a?(String)
|
47
|
+
DateTime.parse(value).new_offset(LOCAL_OFFSET/24).to_time
|
48
|
+
when type <= DateTime && value.respond_to?(:to_datetime)
|
49
|
+
value.to_datetime.new_offset(LOCAL_OFFSET/24)
|
50
|
+
when type <= DateTime && value.is_a?(String)
|
51
|
+
DateTime.parse(value).new_offset(LOCAL_OFFSET/24)
|
52
|
+
when type <= DateTime && value.is_a?(Integer)
|
53
|
+
DateTime.parse(value.to_s).new_offset(LOCAL_OFFSET/24)
|
54
|
+
# Important: DateTime < Date so the order in this case statement matters
|
55
|
+
when type <= Date && value.is_a?(String)
|
56
|
+
Date.parse(value)
|
57
|
+
when type <= Date && value.is_a?(Integer)
|
58
|
+
Date.parse(value.to_s)
|
59
|
+
else
|
60
|
+
raise TypeError, "can't convert `#{value}:#{value.class}` to `#{type}`"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "strongly_typed/attributes"
|
2
|
+
|
3
|
+
module StronglyTyped
|
4
|
+
module Model
|
5
|
+
# @private
|
6
|
+
def self.included(klass)
|
7
|
+
klass.class_eval do
|
8
|
+
extend StronglyTyped::Attributes
|
9
|
+
include StronglyTyped::Coercible
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Entity constructor delegator
|
14
|
+
#
|
15
|
+
# @overload initialize(hsh)
|
16
|
+
# Accepts key values from a hash
|
17
|
+
# @param (see #initialize_from_hash)
|
18
|
+
# @overload initialize(values)
|
19
|
+
# Accepts values from ordered arguments
|
20
|
+
# @param (see #initialize_from_ordered_args)
|
21
|
+
#
|
22
|
+
# @example (see #initialize_from_hash)
|
23
|
+
# @example (see #initialize_from_ordered_args)
|
24
|
+
#
|
25
|
+
# @raise (see #initialize_from_hash)
|
26
|
+
# @raise (see #initialize_from_ordered_args)
|
27
|
+
def initialize(*args)
|
28
|
+
raise ArgumentError, "Need arguments to build your tiny model" if args.empty?
|
29
|
+
|
30
|
+
if args.size == 1 && args.first.kind_of?(Hash)
|
31
|
+
initialize_from_hash(args)
|
32
|
+
else
|
33
|
+
initialize_from_ordered_args(args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Entity constructor from a Hash
|
40
|
+
#
|
41
|
+
# @param [Hash] hsh hash of values
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# class Person
|
45
|
+
# include StronglyTyped::Model
|
46
|
+
#
|
47
|
+
# attribute :id, Integer
|
48
|
+
# attribute :slug, String
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# Person.new(id: 1, slug: 'elgalu')
|
52
|
+
# #=> #<Person:0x00c98 @id=1, @slug="elgalu">
|
53
|
+
# leo.id #=> 1
|
54
|
+
# leo.slug #=> "elgalu"
|
55
|
+
#
|
56
|
+
# @raise [NameError] if tries to assign a non-existing member
|
57
|
+
#
|
58
|
+
# @private
|
59
|
+
def initialize_from_hash(hsh)
|
60
|
+
hsh.first.each_pair do |k, v|
|
61
|
+
if self.class.members.include?(k.to_sym)
|
62
|
+
self.public_send("#{k}=", v)
|
63
|
+
else
|
64
|
+
raise NameError, "Trying to assign non-existing member #{k}=#{v}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Entity constructor from ordered params
|
70
|
+
#
|
71
|
+
# @param [Array] values ordered values
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# class Person
|
75
|
+
# include StronglyTyped::Model
|
76
|
+
#
|
77
|
+
# attribute :id, Integer
|
78
|
+
# attribute :slug, String
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Person.new(1, 'elgalu')
|
82
|
+
# #=> #<Person:0x00c99 @id=1, @slug="elgalu">
|
83
|
+
# leo.id #=> 1
|
84
|
+
# leo.slug #=> "elgalu"
|
85
|
+
#
|
86
|
+
# @raise [ArgumentError] if the arity doesn't match
|
87
|
+
#
|
88
|
+
# @private
|
89
|
+
def initialize_from_ordered_args(values)
|
90
|
+
raise ArgumentError, "wrong number of arguments(#{values.size} for #{self.class.members.size})" if values.size > self.class.members.size
|
91
|
+
|
92
|
+
values.each_with_index do |v, i|
|
93
|
+
# instance_variable_set("@#{self.class.members[i]}", v)
|
94
|
+
self.public_send("#{self.class.members[i]}=", v)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
describe StronglyTyped::Coercible, '#coerce' do
|
5
|
+
|
6
|
+
it 'converts some Fixnum to anything' do
|
7
|
+
coerce(100, to: Float).should eql(100.0)
|
8
|
+
coerce(100, to: String).should eql("100")
|
9
|
+
coerce(100, to: Boolean).should be(true)
|
10
|
+
coerce(100, to: Rational).should eql(Rational(100/1))
|
11
|
+
coerce(100, to: Complex).should eql(Complex(100, 0))
|
12
|
+
coerce(100, to: Time).should be_kind_of(Time)
|
13
|
+
coerce(100, to: Date).should be_kind_of(Date)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'raises exception when trying to convert some Numeric to Symbol' do
|
17
|
+
expect { coerce(100, to: Symbol) }.to raise_error(TypeError)
|
18
|
+
expect { coerce(100.0, to: Symbol) }.to raise_error(TypeError)
|
19
|
+
expect { coerce(9**20, to: Symbol) }.to raise_error(TypeError)
|
20
|
+
expect { coerce(Rational(100/1), to: Symbol) }.to raise_error(TypeError)
|
21
|
+
expect { coerce(Complex(100, 0), to: Symbol) }.to raise_error(TypeError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'converts some Float to anything' do
|
25
|
+
coerce(100.0, to: Integer).should eql(100)
|
26
|
+
coerce(100.0, to: String).should eql("100.0")
|
27
|
+
coerce(100.0, to: Boolean).should be(true)
|
28
|
+
coerce(100.5, to: Rational).should eql(Rational(201.0/2))
|
29
|
+
coerce(100.5, to: Complex).should eql(Complex(100.5, 0))
|
30
|
+
coerce(100.0, to: Time).should be_kind_of(Time)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises exception when trying to convert some Numeric to Date' do
|
34
|
+
expect { coerce(1, to: Date) }.to raise_error(ArgumentError)
|
35
|
+
expect { coerce(1.0, to: Date) }.to raise_error(TypeError)
|
36
|
+
expect { coerce(9**20, to: Date) }.to raise_error(ArgumentError)
|
37
|
+
expect { coerce(Rational(100/1), to: Date) }.to raise_error(TypeError)
|
38
|
+
expect { coerce(Complex(100, 0), to: Date) }.to raise_error(TypeError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'converts dates' do
|
42
|
+
coerce(Date.new(2013, 02, 21), to: DateTime).should == DateTime.new(2013, 02, 21)
|
43
|
+
coerce(Time.new(2013,02,20,10,20,30,0), to: DateTime).should == DateTime.new(2013,02,20,10,20,30,0)
|
44
|
+
coerce(20130221, to: DateTime).should == DateTime.new(2013, 02, 21)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'raises exception when trying to convert some Numeric to DateTime' do
|
48
|
+
expect { coerce(1, to: DateTime) }.to raise_error(ArgumentError)
|
49
|
+
expect { coerce(1.0, to: DateTime) }.to raise_error(TypeError)
|
50
|
+
expect { coerce(9**20, to: DateTime) }.to raise_error(ArgumentError)
|
51
|
+
expect { coerce(Rational(100/1), to: DateTime) }.to raise_error(TypeError)
|
52
|
+
expect { coerce(Complex(100, 0), to: DateTime) }.to raise_error(TypeError)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'raises exception when trying to convert to non primitive types' do
|
56
|
+
expect { coerce(1, to: Object) }.to raise_error(TypeError)
|
57
|
+
expect { coerce(1.0, to: Module) }.to raise_error(TypeError)
|
58
|
+
expect { coerce("str", to: Class) }.to raise_error(TypeError)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'raises exception when trying to convert to Bignum' do
|
62
|
+
expect { coerce(100, to: Bignum) }.to raise_error(TypeError)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
describe StronglyTyped::Model do
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
class Person
|
8
|
+
include StronglyTyped::Model
|
9
|
+
|
10
|
+
attribute :id, Integer
|
11
|
+
attribute :name, String
|
12
|
+
attribute :slug, Symbol
|
13
|
+
attribute :dob, Date
|
14
|
+
attribute :last_seen, DateTime
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when a Person is correctly created on initialize' do
|
19
|
+
let(:leo) { Person.new id: 1122,
|
20
|
+
name: 'Leo Gallucci',
|
21
|
+
slug: :elgalu,
|
22
|
+
dob: '1981-05-28',
|
23
|
+
last_seen: Time.new(2013,02,21,06,37,40,0)
|
24
|
+
}
|
25
|
+
|
26
|
+
it 'should have the correct attribute accessors, values and types' do
|
27
|
+
leo.id.should == Integer(1122)
|
28
|
+
leo.name.should == "Leo Gallucci"
|
29
|
+
leo.slug.should == :elgalu
|
30
|
+
leo.dob.should == Date.new(1981, 05, 28)
|
31
|
+
leo.last_seen.should == DateTime.new(2013,02,21,06,37,40,0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when a Person is initially created with no initial values' do
|
36
|
+
subject { Person.new }
|
37
|
+
|
38
|
+
it 'should raise ArgumentError' do
|
39
|
+
expect { subject }.to raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when a Person is initially created with almost no values' do
|
44
|
+
let(:leo) { Person.new(id: 1) }
|
45
|
+
|
46
|
+
context 'and their members are initialized later on' do
|
47
|
+
it 'is possible to initialize it with correct types' do
|
48
|
+
leo.id = 1122
|
49
|
+
leo.name = 'Leo Gallucci'
|
50
|
+
leo.slug = :elgalu
|
51
|
+
leo.dob = Date.new(1981, 05, 28)
|
52
|
+
leo.last_seen = DateTime.new(2013,02,21,06,37,40,0)
|
53
|
+
# assert:
|
54
|
+
leo.id.should == Integer(1122)
|
55
|
+
leo.name.should == "Leo Gallucci"
|
56
|
+
leo.slug.should == :elgalu
|
57
|
+
leo.dob.should == Date.new(1981, 05, 28)
|
58
|
+
leo.last_seen.should == DateTime.new(2013,02,21,06,37,40,0)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'is possible to intialize it with similar types that will be automatically converted' do
|
62
|
+
leo.id = "1122"
|
63
|
+
leo.name = 'Leo Gallucci'
|
64
|
+
leo.slug = "elgalu"
|
65
|
+
leo.dob = 19810528
|
66
|
+
leo.last_seen = Time.new(2013,02,21,06,37,40,0)
|
67
|
+
# assert:
|
68
|
+
leo.id.should == 1122
|
69
|
+
leo.name.should == "Leo Gallucci"
|
70
|
+
leo.slug.should == :elgalu
|
71
|
+
leo.dob.should == Date.new(1981, 05, 28)
|
72
|
+
leo.last_seen.should == DateTime.new(2013,02,21,06,37,40,0)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when a Person is tried to initialized with invalid types' do
|
78
|
+
it 'raise TypeError when try to coerce nil to Integer' do
|
79
|
+
expect { Person.new id: nil }.to raise_error(TypeError)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'raise TypeError when try to coerce 100 to Symbol' do
|
83
|
+
expect { Person.new slug: 100 }.to raise_error(TypeError)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'raise TypeError when try to coerce nil to Date' do
|
87
|
+
expect { Person.new dob: nil }.to raise_error(TypeError)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'raise TypeError when try to coerce nil to DateTime' do
|
91
|
+
expect { Person.new id: nil }.to raise_error(TypeError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'strongly_typed'
|
2
|
+
require 'simplecov'
|
3
|
+
|
4
|
+
# Require this file using `require "spec_helper"` within each of your specs
|
5
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
8
|
+
config.run_all_when_everything_filtered = true
|
9
|
+
config.filter_run :focus
|
10
|
+
|
11
|
+
# Run specs in random order to surface order dependencies.
|
12
|
+
config.order = 'random'
|
13
|
+
|
14
|
+
# Make coerce() possible at the Example level
|
15
|
+
config.include StronglyTyped::Coercible
|
16
|
+
end
|
17
|
+
|
18
|
+
SimpleCov.start
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'strongly_typed/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
|
8
|
+
gem.platform = Gem::Platform::RUBY
|
9
|
+
gem.name = "strongly_typed"
|
10
|
+
gem.version = StronglyTyped::VERSION
|
11
|
+
gem.summary = %q{Simple type validation for plain ruby object attributes that performs conversions when possible.}
|
12
|
+
gem.description = <<-DESC
|
13
|
+
A simple type validation tool for plain ruby object attributes that performs conversions when possible.
|
14
|
+
Similar to ruby-core Struct but i didn't like using inheritance for something more appropriate for mixins.
|
15
|
+
Check virtus gem if you are looking for a full featured attributes settings for your Ruby Objects that requires complex automatic coercions among other things.
|
16
|
+
If you are looking for a nestable, coercible, Hash-like data structure take a look at structure gem.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
gem.required_ruby_version = '>= 1.9.3'
|
20
|
+
gem.required_rubygems_version = '>= 1.8.11'
|
21
|
+
|
22
|
+
gem.license = 'MIT'
|
23
|
+
|
24
|
+
gem.authors = ["Leo Gallucci"]
|
25
|
+
gem.email = ["elgalu3@gmail.com"]
|
26
|
+
gem.homepage = "https://github.com/elgalu/strongly_typed"
|
27
|
+
|
28
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
29
|
+
gem.files = `git ls-files`.split($/)
|
30
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
31
|
+
gem.require_paths = ["lib"]
|
32
|
+
|
33
|
+
gem.add_runtime_dependency "boolean_class", "~> 0.0"
|
34
|
+
|
35
|
+
gem.add_development_dependency "rake", "~> 10.0"
|
36
|
+
gem.add_development_dependency "rspec", "~> 2.12"
|
37
|
+
gem.add_development_dependency "redcarpet", "~> 2.2"
|
38
|
+
gem.add_development_dependency "yard", "~> 0.8"
|
39
|
+
gem.add_development_dependency "simplecov", "~> 0.7"
|
40
|
+
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: strongly_typed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Leo Gallucci
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: boolean_class
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '10.0'
|
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: '10.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.12'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.12'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redcarpet
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.2'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '2.2'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: yard
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0.8'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0.8'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: simplecov
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.7'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.7'
|
110
|
+
description: ! 'A simple type validation tool for plain ruby object attributes that
|
111
|
+
performs conversions when possible.
|
112
|
+
|
113
|
+
Similar to ruby-core Struct but i didn''t like using inheritance for something more
|
114
|
+
appropriate for mixins.
|
115
|
+
|
116
|
+
Check virtus gem if you are looking for a full featured attributes settings for
|
117
|
+
your Ruby Objects that requires complex automatic coercions among other things.
|
118
|
+
|
119
|
+
If you are looking for a nestable, coercible, Hash-like data structure take a look
|
120
|
+
at structure gem.
|
121
|
+
|
122
|
+
'
|
123
|
+
email:
|
124
|
+
- elgalu3@gmail.com
|
125
|
+
executables: []
|
126
|
+
extensions: []
|
127
|
+
extra_rdoc_files: []
|
128
|
+
files:
|
129
|
+
- .gitignore
|
130
|
+
- .rspec
|
131
|
+
- .travis.yml
|
132
|
+
- .yardopts
|
133
|
+
- Gemfile
|
134
|
+
- LICENSE.txt
|
135
|
+
- README.md
|
136
|
+
- Rakefile
|
137
|
+
- lib/strongly_typed.rb
|
138
|
+
- lib/strongly_typed/attributes.rb
|
139
|
+
- lib/strongly_typed/coercible.rb
|
140
|
+
- lib/strongly_typed/model.rb
|
141
|
+
- lib/strongly_typed/version.rb
|
142
|
+
- spec/coercible_spec.rb
|
143
|
+
- spec/model_spec.rb
|
144
|
+
- spec/spec_helper.rb
|
145
|
+
- strongly_typed.gemspec
|
146
|
+
homepage: https://github.com/elgalu/strongly_typed
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ! '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 1.9.3
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
none: false
|
161
|
+
requirements:
|
162
|
+
- - ! '>='
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: 1.8.11
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 1.8.25
|
168
|
+
signing_key:
|
169
|
+
specification_version: 3
|
170
|
+
summary: Simple type validation for plain ruby object attributes that performs conversions
|
171
|
+
when possible.
|
172
|
+
test_files:
|
173
|
+
- spec/coercible_spec.rb
|
174
|
+
- spec/model_spec.rb
|
175
|
+
- spec/spec_helper.rb
|
176
|
+
has_rdoc:
|