stronger 0.1.0
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.
- 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:
|