domain 0.0.1 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +75 -2
- data/domain.noespec +1 -1
- data/lib/domain.rb +7 -2
- data/lib/domain/api.rb +48 -0
- data/lib/domain/factory.rb +52 -0
- data/lib/domain/factory/reuse.rb +53 -0
- data/lib/domain/factory/sbyc.rb +9 -0
- data/lib/domain/factory/scalar.rb +30 -0
- data/lib/domain/factory/union.rb +11 -0
- data/lib/domain/support.rb +3 -0
- data/lib/domain/support/equalizer.rb +74 -0
- data/lib/domain/support/fake_domain.rb +38 -0
- data/lib/domain/support/impl_domain.rb +17 -0
- data/lib/domain/version.rb +2 -2
- data/spec/factory/reuse_domain/test_new.rb +26 -0
- data/spec/factory/reuse_domain/test_predicate.rb +10 -0
- data/spec/factory/reuse_domain/test_recoat.rb +22 -0
- data/spec/factory/reuse_domain/test_reuse.rb +20 -0
- data/spec/factory/reuse_domain/test_values.rb +11 -0
- data/spec/factory/sbyc_domain/test_new.rb +22 -0
- data/spec/factory/sbyc_domain/test_predicate.rb +10 -0
- data/spec/factory/sbyc_domain/test_sub_domains.rb +14 -0
- data/spec/factory/sbyc_domain/test_super_domain.rb +10 -0
- data/spec/factory/sbyc_domain/test_super_domain_of.rb +18 -0
- data/spec/factory/sbyc_domain/test_triple_equal.rb +23 -0
- data/spec/factory/scalar_domain/test_component_reader.rb +16 -0
- data/spec/factory/scalar_domain/test_hash.rb +14 -0
- data/spec/factory/scalar_domain/test_to_hash.rb +14 -0
- data/spec/factory/scalar_domain/test_values.rb +11 -0
- data/spec/factory/test_sbyc.rb +16 -0
- data/spec/factory/test_scalar.rb +14 -0
- data/spec/factory/test_union.rb +17 -0
- data/spec/factory/union_domain/test_new.rb +17 -0
- data/spec/factory/union_domain/test_predicate.rb +18 -0
- data/spec/factory/union_domain/test_sub_domains.rb +10 -0
- data/spec/factory/union_domain/test_super_domain.rb +10 -0
- data/spec/factory/union_domain/test_super_domain_of.rb +18 -0
- data/spec/factory/union_domain/test_triple_equal.rb +15 -0
- data/spec/fixtures/boolean.rb +3 -0
- data/spec/fixtures/list.rb +6 -0
- data/spec/fixtures/neg_int.rb +3 -0
- data/spec/fixtures/point.rb +3 -0
- data/spec/fixtures/tuple.rb +3 -0
- data/spec/shared/a_domain_class.rb +11 -0
- data/spec/shared/a_value_object.rb +58 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/equalizer/test_new.rb +37 -0
- data/spec/support/test_equalizer.rb +95 -0
- data/spec/test_fixtures.rb +21 -0
- metadata +82 -4
data/README.md
CHANGED
@@ -1,7 +1,80 @@
|
|
1
|
-
# Domain
|
1
|
+
# Domain - Build data-types the easy way
|
2
2
|
|
3
|
-
|
3
|
+
This gem provides modules (module factories in fact) for creating domains, aka data-types, in a various common ways.
|
4
4
|
|
5
5
|
## Links
|
6
6
|
|
7
7
|
https://github.com/blambeau/domain
|
8
|
+
|
9
|
+
## Scalar domains
|
10
|
+
|
11
|
+
A scalar domain has visible components that define the scalar structure of its values. For example a Point domain might have `x` and `y` components.
|
12
|
+
|
13
|
+
# Immediately create a Point class, that is, an implementation of the Point domain
|
14
|
+
# This is similar to `Struct.new(:x, :y)`
|
15
|
+
Point = Domain.scalar(:x, :y)
|
16
|
+
|
17
|
+
# Another way, if you want to define operators for the Point domain
|
18
|
+
class Point
|
19
|
+
extend Domain::Scalar.new(:x, :y)
|
20
|
+
|
21
|
+
def distance
|
22
|
+
Math.sqrt(x*x + y*y)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# in either case, `==`, `eql?` and `hash` are implemented in a consistent way
|
27
|
+
Point.new(1, 2) == Point.new(1, 2)
|
28
|
+
|
29
|
+
## Reuse domains
|
30
|
+
|
31
|
+
A domain can also be built by reuse (the current implementation only support reusing a single value) For instance, a List domain could simply be implemented by reusing an Array.
|
32
|
+
|
33
|
+
class List
|
34
|
+
extend Domain::Reuse.new(Array)
|
35
|
+
|
36
|
+
def head
|
37
|
+
# the reused instance is accessible under `reused_instance` (protected)
|
38
|
+
reused_instance.first
|
39
|
+
end
|
40
|
+
|
41
|
+
# reuse through delegation to `reused_instance`
|
42
|
+
delegate :each, :size, :empty?
|
43
|
+
|
44
|
+
# similar to `delegate` but redecorate the resulting value (will yield Lists)
|
45
|
+
reuse :map, :reject, :select
|
46
|
+
end
|
47
|
+
|
48
|
+
# The reused instance is expected to be passed at construction time
|
49
|
+
List.new [1, 2, 3]
|
50
|
+
|
51
|
+
# `==`, `eql?` and `hash` are already implemented in a consistent way
|
52
|
+
List.new([1, 2]) == List.new([1, 2])
|
53
|
+
|
54
|
+
## Union domains
|
55
|
+
|
56
|
+
A union domain is simply a domain whose values are the union of other domains. The missing Boolean domain can simply be defined as follows:
|
57
|
+
|
58
|
+
# Factors a Boolean domain immediately
|
59
|
+
Boolean = Domain.union(TrueClass, FalseClass)
|
60
|
+
|
61
|
+
# or use the module factory in your own class
|
62
|
+
class Boolean
|
63
|
+
extend Domain::Union.new(TrueClass, FalseClass)
|
64
|
+
end
|
65
|
+
|
66
|
+
# in either case, `===` is implemented in a consistent way
|
67
|
+
Boolean === true
|
68
|
+
Boolean === false
|
69
|
+
|
70
|
+
## Specialization by constraints
|
71
|
+
|
72
|
+
A domain can also be built through specialization by constraint, that is specifying a predicate that will filter the values of a super domain. For instance:
|
73
|
+
|
74
|
+
# Factors the set of positive integers
|
75
|
+
PosInt = Domain.sbyc(Integer){|i| i > 0}
|
76
|
+
|
77
|
+
# or use the module factory (you'll have to extend Integer yourself for consistency)
|
78
|
+
class PosInt < Integer
|
79
|
+
extend Domain::SByC.new(Integer){|i| i > 0}
|
80
|
+
end
|
data/domain.noespec
CHANGED
data/lib/domain.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
+
require_relative 'domain/version'
|
2
|
+
require_relative 'domain/loader'
|
3
|
+
require_relative 'domain/support'
|
4
|
+
require_relative 'domain/api'
|
5
|
+
require_relative 'domain/factory'
|
1
6
|
#
|
2
7
|
# Provide tools for implementing domains (aka data types)
|
3
8
|
#
|
4
9
|
module Domain
|
10
|
+
include API
|
11
|
+
extend Factory
|
5
12
|
|
6
13
|
end # module Domain
|
7
|
-
require "domain/version"
|
8
|
-
require "domain/loader"
|
data/lib/domain/api.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Domain
|
2
|
+
module API
|
3
|
+
|
4
|
+
# Returns the domain predicate, nil if no such predicate
|
5
|
+
#
|
6
|
+
# @return [Proc]
|
7
|
+
# the domain predicate (possibly nil)
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
def predicate
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the super domain of `self`.
|
15
|
+
#
|
16
|
+
# @return [Class]
|
17
|
+
# the super domain of `self` as a ruby class
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def super_domain
|
21
|
+
superclass
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the sub domains of `self`.
|
25
|
+
#
|
26
|
+
# @return [Array]
|
27
|
+
# an array of sub domains (possibly empty)
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def sub_domains
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if this domain is a super domain of `dom`.
|
35
|
+
#
|
36
|
+
# @param [Class] dom
|
37
|
+
# a domain
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
# true if self is a super domain of `dom`, false otherwise
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def super_domain_of?(dom)
|
44
|
+
sub_domains.include?(dom)
|
45
|
+
end
|
46
|
+
|
47
|
+
end # module API
|
48
|
+
end # module Domain
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'factory/union'
|
2
|
+
require_relative 'factory/sbyc'
|
3
|
+
require_relative 'factory/scalar'
|
4
|
+
require_relative 'factory/reuse'
|
5
|
+
module Domain
|
6
|
+
module Factory
|
7
|
+
|
8
|
+
# Creates a domain through specialization by constraint
|
9
|
+
#
|
10
|
+
# @param [Class] super_domain
|
11
|
+
# the super_domain of the factored domain
|
12
|
+
# @param [Proc] pred
|
13
|
+
# the domain predicate
|
14
|
+
#
|
15
|
+
# @return [Class]
|
16
|
+
# the created domain as a ruby Class
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def sbyc(super_domain = Object, &pred)
|
20
|
+
Class.new(super_domain){ extend SByC.new(super_domain, pred) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Factors a union domain
|
24
|
+
#
|
25
|
+
# @param [Class] super_domain
|
26
|
+
# the super_domain of the factored domain
|
27
|
+
# @param [Array] sub_domains
|
28
|
+
# an array of sub domains for the factored domain
|
29
|
+
#
|
30
|
+
# @return [Class]
|
31
|
+
# the created domain as a ruby Class
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def union(*sub_domains)
|
35
|
+
Class.new{ extend Union.new(*sub_domains) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Factors a scalar domain
|
39
|
+
#
|
40
|
+
# @param [Array] component_names
|
41
|
+
# the component names as an array of symbols
|
42
|
+
#
|
43
|
+
# @return [Class]
|
44
|
+
# the created domain as a ruby Class
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def scalar(*component_names)
|
48
|
+
Class.new{ extend Scalar.new(*component_names) }
|
49
|
+
end
|
50
|
+
|
51
|
+
end # module Factory
|
52
|
+
end # module Domain
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Domain
|
2
|
+
module Reuse
|
3
|
+
|
4
|
+
def self.new(reuse_domain, predicate = nil, &bl)
|
5
|
+
ImplDomain.new [ Methods, class_module(predicate || bl) ],
|
6
|
+
[ instance_module(reuse_domain) ]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.class_module(predicate)
|
10
|
+
Module.new{ define_method(:predicate){ predicate } }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.instance_module(reuse_domain)
|
14
|
+
Module.new{
|
15
|
+
define_method(:initialize) do |arg|
|
16
|
+
unless reuse_domain===arg
|
17
|
+
raise ArgumentError, "#{reuse_domain} expected, got `#{arg.inspect}`"
|
18
|
+
end
|
19
|
+
if self.class.predicate && !self.class.predicate.call(arg)
|
20
|
+
raise ArgumentError, "Invalid value `#{arg.inspect}` for `#{self}`"
|
21
|
+
end
|
22
|
+
@reused_instance = arg
|
23
|
+
end
|
24
|
+
define_method(:reused_instance) do
|
25
|
+
@reused_instance
|
26
|
+
end
|
27
|
+
protected :reused_instance
|
28
|
+
include Equalizer.new(:reused_instance)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
module Methods
|
33
|
+
|
34
|
+
def reuse(*methods)
|
35
|
+
methods.each do |m|
|
36
|
+
define_method(m) do |*args, &bl|
|
37
|
+
reused_instance.send(m, *args, &bl)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def recoat(*methods)
|
43
|
+
methods.each do |m|
|
44
|
+
define_method(m) do |*args, &bl|
|
45
|
+
self.class.new reused_instance.send(m, *args, &bl)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end # module Methods
|
51
|
+
|
52
|
+
end # module Reuse
|
53
|
+
end # module Domain
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Domain
|
2
|
+
module Scalar
|
3
|
+
|
4
|
+
def self.new(*component_names)
|
5
|
+
ImplDomain.new [], instance_module(component_names)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.instance_module(component_names)
|
9
|
+
Module.new{
|
10
|
+
define_method(:initialize) do |*args|
|
11
|
+
component_names.zip(args).each do |n,arg|
|
12
|
+
instance_variable_set(:"@#{n}", arg)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
component_names.each do |n|
|
16
|
+
define_method(n) do
|
17
|
+
instance_variable_get(:"@#{n}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
define_method(:to_hash) do
|
21
|
+
component_names.each_with_object({}) do |n,h|
|
22
|
+
h[n] = instance_variable_get(:"@#{n}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
include Equalizer.new(component_names)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
end # class Scalar
|
30
|
+
end # module Domain
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Domain
|
2
|
+
class Equalizer < Module
|
3
|
+
|
4
|
+
# Parts of this module have been extracted from Virtus, MIT Copyright (c) 2011-2012
|
5
|
+
# Piotr Solnica.
|
6
|
+
|
7
|
+
# Creates an Equalizer instance.
|
8
|
+
#
|
9
|
+
# @param [Proc] bl
|
10
|
+
# the proc to use to extract components that participate to equality
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
def initialize(components = nil, &bl)
|
14
|
+
extractor = case components
|
15
|
+
when NilClass then bl
|
16
|
+
when Symbol then proc{ [ send(components) ] }
|
17
|
+
when Array then proc{ components.map{|n| send(n)} }
|
18
|
+
end
|
19
|
+
define_method(:equality_components, &extractor)
|
20
|
+
module_eval{ include(Methods) }
|
21
|
+
end
|
22
|
+
|
23
|
+
module Methods
|
24
|
+
|
25
|
+
# Returns a hash code for the value
|
26
|
+
#
|
27
|
+
# @return Integer
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def hash
|
31
|
+
equality_components.map{|c| c.hash }.reduce(self.class.hash, :^)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Compare the object with other object for equality
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# object.eql?(other) # => true or false
|
38
|
+
#
|
39
|
+
# @param [Object] other
|
40
|
+
# the other object to compare with
|
41
|
+
#
|
42
|
+
# @return [Boolean]
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def eql?(other)
|
46
|
+
instance_of?(other.class) and cmp?(__method__, other)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Compare the object with other object for equivalency
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# object == other # => true or false
|
53
|
+
#
|
54
|
+
# @param [Object] other
|
55
|
+
# the other object to compare with
|
56
|
+
#
|
57
|
+
# @return [Boolean]
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def ==(other)
|
61
|
+
return false unless self.class <=> other.class
|
62
|
+
cmp?(__method__, other)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def cmp?(comparator, other)
|
68
|
+
equality_components.zip(other.equality_components).
|
69
|
+
all?{|l,r| l.send(comparator, r) }
|
70
|
+
end
|
71
|
+
|
72
|
+
end # module Methods
|
73
|
+
end # class Equalizer
|
74
|
+
end # module Domain
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Domain
|
2
|
+
module FakeDomain
|
3
|
+
|
4
|
+
def self.new(super_domain, sub_domains, predicate)
|
5
|
+
Module.new{
|
6
|
+
include Domain, Methods
|
7
|
+
define_method(:super_domain){ super_domain }
|
8
|
+
define_method(:sub_domains) { sub_domains }
|
9
|
+
define_method(:predicate) { predicate }
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
module Methods
|
14
|
+
|
15
|
+
# Creates a new instance of this domain
|
16
|
+
def new(*args)
|
17
|
+
if (args.size == 1) && self===args.first
|
18
|
+
return args.first
|
19
|
+
elsif superclass.respond_to?(:new) && (superclass != Object)
|
20
|
+
return new super(*args)
|
21
|
+
end
|
22
|
+
args_error_on_new(args)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Checks if `value` belongs to this domain
|
26
|
+
def ===(value)
|
27
|
+
value.is_a?(superclass) && predicate && predicate.call(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def args_error_on_new(args)
|
33
|
+
raise ArgumentError, "Invalid value #{args.join(' ')} for #{self}", caller
|
34
|
+
end
|
35
|
+
|
36
|
+
end # module Methods
|
37
|
+
end # module FakeDomain
|
38
|
+
end # module Domain
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Domain
|
2
|
+
module ImplDomain
|
3
|
+
|
4
|
+
def self.new(c_methods = [], i_methods = [])
|
5
|
+
i_methods = Array(i_methods)
|
6
|
+
c_methods = Array(c_methods).unshift(Domain)
|
7
|
+
Module.new{
|
8
|
+
c_methods.each{|c_m| include(c_m)}
|
9
|
+
define_singleton_method(:extend_object) do |obj|
|
10
|
+
obj.module_eval{ i_methods.each{|i_m| include(i_m)} } if obj.is_a?(Class)
|
11
|
+
super(obj)
|
12
|
+
end
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
end # module ImplDomain
|
17
|
+
end # module Domain
|