stronger 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/LICENSE +17 -0
- data/README.md +105 -0
- data/Rakefile +8 -0
- data/TODO +1 -0
- data/lib/stronger.rb +7 -0
- data/lib/stronger/array.rb +16 -0
- data/lib/stronger/collection.rb +27 -0
- data/lib/stronger/hash.rb +13 -0
- data/lib/stronger/interface.rb +8 -0
- data/lib/stronger/property.rb +19 -0
- data/lib/stronger/property_definition.rb +68 -0
- data/lib/stronger/property_set.rb +79 -0
- data/lib/stronger/type_checking.rb +55 -0
- data/lib/stronger/version.rb +3 -0
- data/stronger.gemspec +17 -0
- data/test/array_test.rb +37 -0
- data/test/ex_assertions.rb +21 -0
- data/test/hash_test.rb +19 -0
- data/test/property_definition_test.rb +55 -0
- data/test/property_set_test.rb +123 -0
- data/test/test_helper.rb +6 -0
- data/test/type_checking_test.rb +68 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 61d3f70e5efe9e26b9972e5686bb01a54b068338
|
4
|
+
data.tar.gz: 430212356514e889ac68086bf07599ac3623b6a3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1ddf9da54b794322d601dab9eb27c1acea7cc6627cf71a50ee9ef7e7ed9ce2a8a6f7abab1b08b103dcedc836c7ed6842a4d08a8c54584e1095a2b54822acfe53
|
7
|
+
data.tar.gz: d13e59be287c071627738c6fdf898797173a387b959255f15fe1f5ce267881a2afdf65ad37ea365629eaadc9fe262c3f1695ac4a2b3bca3ea3e23c342888ef29
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/LICENSE
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
2
|
+
this software and associated documentation files (the "Software"), to deal in
|
3
|
+
the Software without restriction, including without limitation the rights to
|
4
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
5
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
6
|
+
so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in all
|
9
|
+
copies or substantial portions of the Software.
|
10
|
+
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
17
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# Stronger
|
2
|
+
|
3
|
+
Stronger is a basic run-time type-safety kit for Ruby, which introduces a couple
|
4
|
+
of tools for simple run-time type checking.
|
5
|
+
|
6
|
+
There's nothing revolutionary here. Just a few convenient types and methods to
|
7
|
+
ease the burden of the type-conscious rubyist.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
`gem install stronger` and `require 'stronger'` in your code.
|
12
|
+
|
13
|
+
## Features
|
14
|
+
|
15
|
+
### Collections
|
16
|
+
|
17
|
+
Stronger provides some type-checking wrappers around Ruby's Hash and Array
|
18
|
+
classes, which help give you a bit of type-safety in the values you can expect
|
19
|
+
from these collections.
|
20
|
+
|
21
|
+
##### Array
|
22
|
+
|
23
|
+
Typed arrays check anything appended.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
arr = Stronger::TypedArray.new(Integer)
|
27
|
+
arr.push 5
|
28
|
+
|
29
|
+
arr.push 4 # raises TypeError
|
30
|
+
```
|
31
|
+
|
32
|
+
They also refuse to concatenate with other TypedArrays not of the same type.
|
33
|
+
|
34
|
+
##### Hash
|
35
|
+
|
36
|
+
Typed hashes check values added to ensure type.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
hash = Stronger::TypedHash.new(String)
|
40
|
+
hash[:first] = 'Groovy'
|
41
|
+
hash[2] = :woah_there # raises TypeError
|
42
|
+
```
|
43
|
+
|
44
|
+
### Properties
|
45
|
+
|
46
|
+
Some class methods are provided in the `PropertyDefinition` module which allows
|
47
|
+
you to define properties on classes. Properties are required by default,
|
48
|
+
can be set via the constructor, the private `set_properties` method, or setters.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class Person
|
52
|
+
property :name, type: String
|
53
|
+
property :phone_number, type: String, required: false
|
54
|
+
end
|
55
|
+
|
56
|
+
addr = AddressBookEntry.new(name: "Boris")
|
57
|
+
addr.name #=> 'Boris'
|
58
|
+
addr.name = 3 # Raises Stronger::InvalidProperty
|
59
|
+
```
|
60
|
+
|
61
|
+
|
62
|
+
### Interfaces
|
63
|
+
|
64
|
+
Stronger has a very simple (and very dumb) concept of an `Interface`,
|
65
|
+
which is just a list of methods objects must respond to encapsulated in an
|
66
|
+
object. These `Interface` objects can be treated as types however, which makes
|
67
|
+
duck-typing with Collections and Properties a lot nicer.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
AnimalInterface = Stronger::Interface.new(:move, :make_noise)
|
71
|
+
|
72
|
+
class Dog
|
73
|
+
def move
|
74
|
+
end
|
75
|
+
|
76
|
+
def make_noise
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
The `AnimalInterface` defined above can be used in place of classes for types
|
82
|
+
in Stronger's properties and typed collections.
|
83
|
+
|
84
|
+
### Type checking methods
|
85
|
+
|
86
|
+
The `Stronger::TypeChecking` refinement provides a `is_strong?` method on
|
87
|
+
`Object` which allows you to check types yourself. It behaves like `is_a?`
|
88
|
+
but includes support for interface checking.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
use Stronger::TypeChecking
|
92
|
+
|
93
|
+
BoatInterface = Stronger::Interface.new(:float)
|
94
|
+
|
95
|
+
Object.new.is_strong?(Object) # => true
|
96
|
+
Object.new.is_strong?(Boat) # => false
|
97
|
+
```
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
Fork and open a PR. Please include tests and ensure `rake test` runs.
|
102
|
+
|
103
|
+
## License
|
104
|
+
|
105
|
+
See LICENSE
|
data/Rakefile
ADDED
data/lib/stronger.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'stronger/collection'
|
2
|
+
module Stronger
|
3
|
+
class TypedArray < ::Array
|
4
|
+
include Collection
|
5
|
+
[:push, :shift, :[]=, :<<].each do |name|
|
6
|
+
define_method(name) do |value|
|
7
|
+
check_value_type!(value)
|
8
|
+
super(value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def concat(arr)
|
13
|
+
check_collection_type!(arr)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stronger
|
2
|
+
module Collection
|
3
|
+
Interface = Stronger::Interface.new(:type)
|
4
|
+
using TypeChecking
|
5
|
+
|
6
|
+
attr_reader :type
|
7
|
+
def initialize(type, *rest)
|
8
|
+
@type = type
|
9
|
+
super(*rest)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def check_value_type!(val)
|
15
|
+
unless val.is_strong?(type)
|
16
|
+
raise TypeError, "#{self} expects values of type: #{type}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_collection_type!(col)
|
21
|
+
unless col.implements?(Collection::Interface) and col.type < type
|
22
|
+
raise TypeError,
|
23
|
+
"#{self} may only be concatenated with an array of the same type!"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Stronger
|
2
|
+
class Property
|
3
|
+
attr_reader :name, :type
|
4
|
+
def initialize(name, type:, required: true)
|
5
|
+
@name, @type, @required = name, type, required
|
6
|
+
end
|
7
|
+
|
8
|
+
def required?
|
9
|
+
!!required
|
10
|
+
end
|
11
|
+
|
12
|
+
def typed?
|
13
|
+
!type.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
attr_reader :required
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Stronger
|
2
|
+
module PropertyDefinition
|
3
|
+
module ClassMethods
|
4
|
+
def properties
|
5
|
+
@properties ||= Array.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def property_names
|
9
|
+
properties.map(&:name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def property(*names, **opts)
|
13
|
+
new_props = names.map {|name| Property.new(name, **opts) }
|
14
|
+
properties.concat(new_props)
|
15
|
+
new_props.each{|p| expose_property(p) unless opts.delete(:private)}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def expose_property(property)
|
21
|
+
define_property_method(property.name) do |properties|
|
22
|
+
properties[property.name]
|
23
|
+
end
|
24
|
+
|
25
|
+
define_property_method("#{property.name}=") do |properties, value|
|
26
|
+
properties[property.name] = value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Defines a method by the given name using a given block, to which
|
31
|
+
# the properties on the instance on which the method was called
|
32
|
+
# will be yielded as the first argument, and the arguments given to
|
33
|
+
# the called method as the rest of the arguments. PropertyErrors
|
34
|
+
# raised due to operations on the yielded properties will be rescued
|
35
|
+
# and re-raised with their stack trace origin set to the defined method.
|
36
|
+
def define_property_method(name, &blk)
|
37
|
+
define_method(name) do |*args|
|
38
|
+
begin
|
39
|
+
blk.call(properties, *args)
|
40
|
+
rescue PropertyError => e
|
41
|
+
e.set_backtrace caller
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.included(base)
|
49
|
+
base.extend(ClassMethods)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(**property_values)
|
53
|
+
set_properties(property_values)
|
54
|
+
rescue PropertyError => e
|
55
|
+
e.set_backtrace caller
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
attr_reader :properties
|
61
|
+
|
62
|
+
def set_properties(values)
|
63
|
+
@properties = PropertySet.new(values, self.class.properties)
|
64
|
+
rescue PropertyError => e
|
65
|
+
raise e
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class PropertyError < StandardError; end
|
5
|
+
class InvalidProperty < PropertyError; end
|
6
|
+
class UndefinedProperty < PropertyError; end
|
7
|
+
class MissingProperty < PropertyError; end
|
8
|
+
|
9
|
+
class PropertySet
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
# TODO: given_props should be called values
|
13
|
+
def initialize(given_values, config_props)
|
14
|
+
@config_props = config_props
|
15
|
+
@values = Hash.new
|
16
|
+
|
17
|
+
given_values.each {|k,v| set(k, v) }
|
18
|
+
validate_required_present!
|
19
|
+
end
|
20
|
+
def_delegator :values, :include?
|
21
|
+
|
22
|
+
def []=(key, value)
|
23
|
+
property = get_property(key)
|
24
|
+
validate_type!(value, property)
|
25
|
+
values[key] = value
|
26
|
+
end
|
27
|
+
alias_method :set, :[]=
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
get_property(key)
|
31
|
+
values[key] || (raise MissingProperty,
|
32
|
+
"#{key} is not required and has not been set!")
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(name)
|
36
|
+
unless get_property(name).required?
|
37
|
+
values.delete(name)
|
38
|
+
else
|
39
|
+
raise MissingProperty, "You can't delete required property #{name}!"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
attr_reader :config_props, :values
|
45
|
+
|
46
|
+
def values
|
47
|
+
@values ||= Hash.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_type!(value, property)
|
51
|
+
unless value.is_a?(property.type)
|
52
|
+
raise InvalidProperty,
|
53
|
+
"Property #{property.name} should be a #{property.type}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_property(name)
|
58
|
+
property = config_props.find{|p| p.name == name}
|
59
|
+
return property ||
|
60
|
+
(raise UndefinedProperty, "#{name} is not a valid property!")
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_required_present!
|
64
|
+
config_props.select{|p| p.required?}.each do |property|
|
65
|
+
values.fetch(property.name) do
|
66
|
+
raise MissingProperty, "Property #{property.name} is required!"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def extra_property_message(extra_property_names)
|
72
|
+
if extra_property_names.length > 1
|
73
|
+
"#{extra_property_names} are not recognized property names!"
|
74
|
+
else
|
75
|
+
"#{extra_property_names.first} is not a valid property name!"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Stronger
|
2
|
+
module TypeChecking
|
3
|
+
refine Object do
|
4
|
+
def is_strong?(type)
|
5
|
+
if type.is_a?(Interface)
|
6
|
+
implements?(type)
|
7
|
+
elsif type.is_a?(Class) or type.is_a?(Module)
|
8
|
+
is_a?(type)
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Don't know how to compare type of "\
|
11
|
+
"#{self} against #{type}. Try using a "\
|
12
|
+
"Stronger::Interface, Class or Module"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def implements?(interface)
|
17
|
+
interface.methods.all?{|m| respond_to?(m) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Implementers
|
22
|
+
def <(type)
|
23
|
+
if type.is_a?(Interface)
|
24
|
+
instance_implements?(type)
|
25
|
+
else
|
26
|
+
super(type)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def >(type)
|
31
|
+
!(self <= type)
|
32
|
+
end
|
33
|
+
|
34
|
+
def instance_implements?(interface)
|
35
|
+
(interface.methods - instance_methods).empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
def implement(interface)
|
39
|
+
interface.methods.each do |name|
|
40
|
+
define_method(name) do
|
41
|
+
raise NotImplementedError, "#{self.class} should implement #{name}!"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
refine Class do
|
48
|
+
include Implementers
|
49
|
+
end
|
50
|
+
|
51
|
+
refine Module do
|
52
|
+
include Implementers
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/stronger.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'stronger/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'stronger'
|
6
|
+
s.version = Stronger::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ['Jack Forrest']
|
9
|
+
s.email = ['jack@jrforrest.net']
|
10
|
+
s.homepage = 'https://github.com/jrforrest/stronger'
|
11
|
+
s.summary = 'Run-time type checking utils'
|
12
|
+
s.description = 'Provides several utilities for run-time type-checking with Ruby'
|
13
|
+
s.license = 'MIT'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files test`.split("\n")
|
17
|
+
end
|
data/test/array_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class ArrayTest < MiniTest::Test
|
5
|
+
include ExAssertions
|
6
|
+
|
7
|
+
DuckInterface = Interface.new(:quack)
|
8
|
+
class Duck
|
9
|
+
def quack; end
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_array
|
13
|
+
array = TypedArray.new(DuckInterface)
|
14
|
+
refute_ex_raised TypeError, ->{array.push(Duck.new)},
|
15
|
+
"The array allows objects of its configured type to be pushed."
|
16
|
+
assert_ex_raised TypeError, ->{array.push(Object.new)},
|
17
|
+
"Types not implementing the array's type will raise a TypeError "\
|
18
|
+
"when pushed."
|
19
|
+
assert_ex_raised TypeError, -> {array << Object.new},
|
20
|
+
"Type checking works on << alias for array"
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_concat
|
24
|
+
assert_ex_raised TypeError,
|
25
|
+
->{ TypedArray.new(DuckInterface).concat(::Array.new) },
|
26
|
+
"Can not concatenate a typed array with an un-typed array."
|
27
|
+
assert_ex_raised TypeError,
|
28
|
+
->{ TypedArray.new(String).concat(Array.new(Fixnum)) },
|
29
|
+
"Can not concatenate a typed array with a typed array "\
|
30
|
+
"with an incompatible type"
|
31
|
+
refute_ex_raised TypeError,
|
32
|
+
->{ TypedArray.new(Numeric).concat(TypedArray.new(Fixnum)) },
|
33
|
+
"A typed array may be concatenated with another typed array which has "\
|
34
|
+
"a type that implements its own type."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A couple of assertions for testing exceptions.
|
2
|
+
#
|
3
|
+
# These already exist in MiniTest but I don't like the way they work.
|
4
|
+
module ExAssertions
|
5
|
+
private
|
6
|
+
|
7
|
+
def refute_ex_raised(ex, lam, message = nil)
|
8
|
+
refute(raised?(ex, &lam), message)
|
9
|
+
end
|
10
|
+
|
11
|
+
def assert_ex_raised(ex, lam, message = nil)
|
12
|
+
assert(raised?(ex, &lam), message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def raised?(ex)
|
16
|
+
yield
|
17
|
+
return false
|
18
|
+
rescue ex => e
|
19
|
+
return true
|
20
|
+
end
|
21
|
+
end
|
data/test/hash_test.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class HashTest < MiniTest::Test
|
5
|
+
include ExAssertions
|
6
|
+
|
7
|
+
def test_hash
|
8
|
+
hash = TypedHash.new(String)
|
9
|
+
assert_ex_raised TypeError, ->{ hash[:one] = 1 },
|
10
|
+
"Trying to set a value in the hash which is not of the type "\
|
11
|
+
"with which the hash was instantiated should raise a TypeError"
|
12
|
+
hash[:two] = "2"
|
13
|
+
assert_equal "2", hash[:two],
|
14
|
+
"Setting a hash value with the appropriate type should work."
|
15
|
+
assert_ex_raised TypeError, ->{ hash.store(:three, 3)},
|
16
|
+
"Store shouldn't work either."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class PropertyDefinitionTest < MiniTest::Test
|
5
|
+
include ExAssertions
|
6
|
+
|
7
|
+
class PropertyDefinitionDummy
|
8
|
+
include PropertyDefinition
|
9
|
+
|
10
|
+
property :name, type: String
|
11
|
+
property :address, type: String, required: false
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_valid
|
15
|
+
assert valid_dummy,
|
16
|
+
"The valid dummy should instantiate with no errors"
|
17
|
+
assert_equal valid_values[:name], valid_dummy.name,
|
18
|
+
"The name method should expose the name"
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_invalid
|
22
|
+
assert_ex_raised MissingProperty, ->{invalid_dummy},
|
23
|
+
"Creating an instance without all of the required properties "\
|
24
|
+
"should result in a MissingProperty exception"
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_set_invalid
|
28
|
+
assert_ex_raised InvalidProperty, ->{valid_dummy.address = 25},
|
29
|
+
"Attempting to set a property to the wrong type should raise an "\
|
30
|
+
"InvalidProperty exception"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def valid_dummy
|
36
|
+
@valid_dummy ||= make_dummy(valid_values)
|
37
|
+
end
|
38
|
+
|
39
|
+
def invalid_dummy
|
40
|
+
@invalid_dummuy ||= make_dummy(invalid_values)
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_dummy(values)
|
44
|
+
PropertyDefinitionDummy.new(**values)
|
45
|
+
end
|
46
|
+
|
47
|
+
def invalid_values
|
48
|
+
{address: "one"}
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid_values
|
52
|
+
{name: "Riiiiiiick!"}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class PropertySetTest < Minitest::Test
|
5
|
+
include ExAssertions
|
6
|
+
|
7
|
+
def test_valid_properties
|
8
|
+
refute_ex_raised PropertyError, ->{make_set(valid_properties)},
|
9
|
+
"Creating a propety set with valid values should not raise "\
|
10
|
+
"an exception"
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_extra_properties
|
14
|
+
assert_ex_raised UndefinedProperty, ->{make_set(extra_properties)},
|
15
|
+
"Extra values given to a property set should result in an "\
|
16
|
+
"InvalidProperty exception"
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_invalid_type
|
20
|
+
assert_ex_raised InvalidProperty,
|
21
|
+
->{make_set(improperly_typed_properties)},
|
22
|
+
"Giving values with the wrong type should result in "\
|
23
|
+
"an InvalidProperty exception"
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_missing_required
|
27
|
+
assert_ex_raised MissingProperty, ->{make_set(insufficient_properties)},
|
28
|
+
"Failing to supply required values should result in a MissingProperty "\
|
29
|
+
"error"
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_value_accessors
|
33
|
+
name = 'Baron Bower'
|
34
|
+
|
35
|
+
valid_set[:name] = name
|
36
|
+
assert_equal name, valid_set[:name],
|
37
|
+
"A property can be set and get using hash syntax"
|
38
|
+
assert_ex_raised UndefinedProperty, ->{valid_set[:bobbyhill]},
|
39
|
+
"Attempting to access a property not defined on the set "\
|
40
|
+
"should result in an undefined property exception"
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_set_invalid_value
|
44
|
+
assert_ex_raised InvalidProperty, ->{valid_set[:name] = 25},
|
45
|
+
"Attempting to set a property to an invalid value should raise an "\
|
46
|
+
"invalid property exception"
|
47
|
+
assert_ex_raised UndefinedProperty, ->{valid_set[:bobbyhill] = 25},
|
48
|
+
"Attempting to set a property which does not exist should result "\
|
49
|
+
"in an undefined property error"
|
50
|
+
assert_ex_raised InvalidProperty, ->{valid_set[:name] = nil},
|
51
|
+
"Attempting to make a property nil should result in "\
|
52
|
+
" an InvalidProperty exception"
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_delete_value
|
56
|
+
address = valid_set[:address]
|
57
|
+
assert_equal address, valid_set.delete(:address),
|
58
|
+
"Deleting a non-required value should return that value"
|
59
|
+
assert_ex_raised MissingProperty, ->{valid_set.delete(:name)},
|
60
|
+
"Deleting a required value should result in a MissingProperty "\
|
61
|
+
"exception"
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_access_missing_value
|
65
|
+
assert_ex_raised MissingProperty, ->{partial_valid_set[:address]},
|
66
|
+
"A MissingProperty error should be raised when a property "\
|
67
|
+
"which is not present is accessed"
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_include?
|
71
|
+
refute partial_valid_set.include?(:address),
|
72
|
+
"Include returns false for a value which is not present"
|
73
|
+
|
74
|
+
partial_valid_set[:address] = "hiya"
|
75
|
+
assert partial_valid_set.include?(:address),
|
76
|
+
"Include returns true for a value which is present"
|
77
|
+
|
78
|
+
partial_valid_set.delete(:address)
|
79
|
+
refute partial_valid_set.include?(:address),
|
80
|
+
"Include still returns false for a valid which was present "\
|
81
|
+
"but was deleted"
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def partial_valid_set
|
88
|
+
@partial_avlid_set ||=
|
89
|
+
make_set(valid_properties.tap{|p| p.delete :address})
|
90
|
+
end
|
91
|
+
|
92
|
+
def valid_set
|
93
|
+
@valid_set ||= make_set(valid_properties)
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_set(properties)
|
97
|
+
PropertySet.new(properties, configured_properties)
|
98
|
+
end
|
99
|
+
|
100
|
+
def configured_properties
|
101
|
+
@props ||= [
|
102
|
+
Property.new(:name, type: String, required: true),
|
103
|
+
Property.new(:address, type: String, required: false) ]
|
104
|
+
end
|
105
|
+
|
106
|
+
def insufficient_properties
|
107
|
+
valid_properties.tap{|h| h.delete(:name) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def improperly_typed_properties
|
111
|
+
valid_properties.tap {|h| h[:name] = 25}
|
112
|
+
end
|
113
|
+
|
114
|
+
def valid_properties
|
115
|
+
{ name: "Joseph Miller",
|
116
|
+
address: "2344 whatever dr. Raleigh NC 27610" }
|
117
|
+
end
|
118
|
+
|
119
|
+
def extra_properties
|
120
|
+
valid_properties.merge({dog: 'Betty Beagle'})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Stronger
|
4
|
+
class TypeCheckingTest < MiniTest::Test
|
5
|
+
include ExAssertions
|
6
|
+
using TypeChecking
|
7
|
+
|
8
|
+
DuckInterface = Interface.new(:quack, :walk)
|
9
|
+
KickInterface = Interface.new(:give_it_the_boot)
|
10
|
+
|
11
|
+
module Puntable
|
12
|
+
implement KickInterface
|
13
|
+
end
|
14
|
+
|
15
|
+
class Duck
|
16
|
+
implement DuckInterface
|
17
|
+
include Puntable
|
18
|
+
|
19
|
+
def quack; end
|
20
|
+
def walk; end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Goose
|
24
|
+
def quack; end
|
25
|
+
def walk; end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_class_type
|
29
|
+
assert String.new.is_strong?(String),
|
30
|
+
"Stronger typecheck should match on the class of the object"
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_interface_instance
|
34
|
+
assert duck.is_strong?(DuckInterface),
|
35
|
+
"An object responding to all of the methods required by an "\
|
36
|
+
"interface should be identify with that interface as a type"
|
37
|
+
assert duck.is_strong?(Duck),
|
38
|
+
"An object of a class should be identified with that class as "\
|
39
|
+
"a type."
|
40
|
+
assert duck.is_strong?(Puntable),
|
41
|
+
"An object belonging to a class which includes a module should "\
|
42
|
+
"be identified as a type of that module."
|
43
|
+
assert_ex_raised NotImplementedError, ->{duck.give_it_the_boot},
|
44
|
+
"An object belonging to an interface should have that method "\
|
45
|
+
"defined with a default behavior of raising a NotImplementedError"
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_ducktyped_interface
|
49
|
+
assert Goose.instance_implements?(DuckInterface),
|
50
|
+
"Even a class which does not explicitly implement an interface will "\
|
51
|
+
"identify with that interface if it implements all "\
|
52
|
+
"the necessary methods"
|
53
|
+
refute Goose.instance_implements?(KickInterface),
|
54
|
+
"A class does not implement an interface if it does not satisfy "\
|
55
|
+
"all of its methods"
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_instance_implements
|
59
|
+
assert Duck.instance_implements?(KickInterface)
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def duck
|
65
|
+
@duck ||= Duck.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stronger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jack Forrest
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-28 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Provides several utilities for run-time type-checking with Ruby
|
14
|
+
email:
|
15
|
+
- jack@jrforrest.net
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".gitignore"
|
21
|
+
- LICENSE
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- TODO
|
25
|
+
- lib/stronger.rb
|
26
|
+
- lib/stronger/array.rb
|
27
|
+
- lib/stronger/collection.rb
|
28
|
+
- lib/stronger/hash.rb
|
29
|
+
- lib/stronger/interface.rb
|
30
|
+
- lib/stronger/property.rb
|
31
|
+
- lib/stronger/property_definition.rb
|
32
|
+
- lib/stronger/property_set.rb
|
33
|
+
- lib/stronger/type_checking.rb
|
34
|
+
- lib/stronger/version.rb
|
35
|
+
- stronger.gemspec
|
36
|
+
- test/array_test.rb
|
37
|
+
- test/ex_assertions.rb
|
38
|
+
- test/hash_test.rb
|
39
|
+
- test/property_definition_test.rb
|
40
|
+
- test/property_set_test.rb
|
41
|
+
- test/test_helper.rb
|
42
|
+
- test/type_checking_test.rb
|
43
|
+
homepage: https://github.com/jrforrest/stronger
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 2.4.8
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Run-time type checking utils
|
67
|
+
test_files:
|
68
|
+
- test/array_test.rb
|
69
|
+
- test/ex_assertions.rb
|
70
|
+
- test/hash_test.rb
|
71
|
+
- test/property_definition_test.rb
|
72
|
+
- test/property_set_test.rb
|
73
|
+
- test/test_helper.rb
|
74
|
+
- test/type_checking_test.rb
|
75
|
+
has_rdoc:
|