axiom-types 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.
- checksums.yaml +15 -0
- data/.gitignore +37 -0
- data/.rspec +4 -0
- data/.rvmrc +1 -0
- data/.travis.yml +35 -0
- data/.yardopts +4 -0
- data/Gemfile +10 -0
- data/Gemfile.devtools +62 -0
- data/Guardfile +24 -0
- data/LICENSE +20 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/TODO +25 -0
- data/axiom-types.gemspec +26 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +100 -0
- data/config/roodi.yml +16 -0
- data/config/yardstick.yml +2 -0
- data/lib/axiom-types.rb +3 -0
- data/lib/axiom/types.rb +65 -0
- data/lib/axiom/types/array.rb +13 -0
- data/lib/axiom/types/boolean.rb +14 -0
- data/lib/axiom/types/class.rb +13 -0
- data/lib/axiom/types/date.rb +18 -0
- data/lib/axiom/types/date_time.rb +21 -0
- data/lib/axiom/types/decimal.rb +13 -0
- data/lib/axiom/types/encodable.rb +75 -0
- data/lib/axiom/types/float.rb +13 -0
- data/lib/axiom/types/hash.rb +45 -0
- data/lib/axiom/types/integer.rb +13 -0
- data/lib/axiom/types/length_comparable.rb +51 -0
- data/lib/axiom/types/numeric.rb +13 -0
- data/lib/axiom/types/object.rb +36 -0
- data/lib/axiom/types/set.rb +13 -0
- data/lib/axiom/types/string.rb +18 -0
- data/lib/axiom/types/support/options.rb +129 -0
- data/lib/axiom/types/symbol.rb +18 -0
- data/lib/axiom/types/time.rb +22 -0
- data/lib/axiom/types/type.rb +136 -0
- data/lib/axiom/types/value_comparable.rb +49 -0
- data/lib/axiom/types/version.rb +10 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/unit/axiom/types/class_methods/finalize_spec.rb +22 -0
- data/spec/unit/axiom/types/encodable/class_methods/extended_spec.rb +31 -0
- data/spec/unit/axiom/types/encodable/finalize_spec.rb +55 -0
- data/spec/unit/axiom/types/hash/class_methods/finalize_spec.rb +55 -0
- data/spec/unit/axiom/types/length_comparable/class_methods/extended_spec.rb +32 -0
- data/spec/unit/axiom/types/length_comparable/finalize_spec.rb +32 -0
- data/spec/unit/axiom/types/object/class_methods/coercion_method_spec.rb +27 -0
- data/spec/unit/axiom/types/object/class_methods/finalize_spec.rb +29 -0
- data/spec/unit/axiom/types/object/class_methods/primitive_spec.rb +27 -0
- data/spec/unit/axiom/types/options/accept_options_spec.rb +98 -0
- data/spec/unit/axiom/types/options/inherited_spec.rb +38 -0
- data/spec/unit/axiom/types/type/class_methods/constraint_spec.rb +32 -0
- data/spec/unit/axiom/types/type/class_methods/finalize_spec.rb +20 -0
- data/spec/unit/axiom/types/type/class_methods/include_predicate_spec.rb +25 -0
- data/spec/unit/axiom/types/type/class_methods/includes_spec.rb +45 -0
- data/spec/unit/axiom/types/type/class_methods/new_spec.rb +79 -0
- data/spec/unit/axiom/types/value_comparable/class_methods/extended_spec.rb +32 -0
- data/spec/unit/axiom/types/value_comparable/finalize_spec.rb +32 -0
- metadata +215 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Represents a string type
|
7
|
+
class String < Object
|
8
|
+
extend LengthComparable, Encodable
|
9
|
+
|
10
|
+
primitive ::String
|
11
|
+
coercion_method :to_string
|
12
|
+
|
13
|
+
minimum_length 0
|
14
|
+
maximum_length 255
|
15
|
+
|
16
|
+
end # class String
|
17
|
+
end # module Types
|
18
|
+
end # module Axiom
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# A module that adds class and instance level options
|
7
|
+
module Options
|
8
|
+
|
9
|
+
# Raised when the method is already used
|
10
|
+
class ReservedMethodError < ArgumentError; end
|
11
|
+
|
12
|
+
# Defines which options are valid for a given attribute class
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class MyTypes < Axiom::Types::Object
|
16
|
+
# accept_options :foo, :bar
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @return [self]
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
def accept_options(*new_options)
|
23
|
+
(new_options - accepted_options).each do |new_option|
|
24
|
+
assert_method_available(new_option)
|
25
|
+
add_accepted_option(new_option)
|
26
|
+
define_option_method(new_option)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# Adds new option that an attribute class can accept
|
34
|
+
#
|
35
|
+
# @param [Symbol] new_option
|
36
|
+
# new option to be added
|
37
|
+
#
|
38
|
+
# @return [self]
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
def add_accepted_option(new_option)
|
42
|
+
accepted_options << new_option
|
43
|
+
descendants.each do |descendant|
|
44
|
+
descendant.send(__method__, new_option)
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Adds descendant to descendants array and inherits default options
|
52
|
+
#
|
53
|
+
# @param [Class] descendant
|
54
|
+
#
|
55
|
+
# @return [undefined]
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
def inherited(descendant)
|
59
|
+
super
|
60
|
+
options.each do |option, value|
|
61
|
+
descendant.add_accepted_option(option).public_send(option, value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns default options hash for a given attribute class
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Axiom::Types::String.options
|
69
|
+
# # => {:primitive => String}
|
70
|
+
#
|
71
|
+
# @return [Hash]
|
72
|
+
# a hash of default option values
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
def options
|
76
|
+
accepted_options.each_with_object({}) do |name, options|
|
77
|
+
options[name] = public_send(name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of valid options
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# Axiom::Types::String.accepted_options
|
85
|
+
# # => [:primitive, :accessor, :reader, :writer]
|
86
|
+
#
|
87
|
+
# @return [Array]
|
88
|
+
# the array of valid option names
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
def accepted_options
|
92
|
+
@accepted_options ||= []
|
93
|
+
end
|
94
|
+
|
95
|
+
# Assert that the option is not already defined
|
96
|
+
#
|
97
|
+
# @param [Symbol] name
|
98
|
+
#
|
99
|
+
# @return [undefined]
|
100
|
+
#
|
101
|
+
# @raise [ReservedMethodError]
|
102
|
+
# raised when the method is already defined
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
def assert_method_available(name)
|
106
|
+
return unless respond_to?(name)
|
107
|
+
raise ReservedMethodError,
|
108
|
+
"method named `#{name.inspect}` is already defined"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Adds a reader/writer method for the give option name
|
112
|
+
#
|
113
|
+
# @param [#to_s] name
|
114
|
+
#
|
115
|
+
# @return [undefined]
|
116
|
+
#
|
117
|
+
# @api private
|
118
|
+
def define_option_method(name)
|
119
|
+
ivar = "@#{name}"
|
120
|
+
define_singleton_method(name) do |*args|
|
121
|
+
return instance_variable_get(ivar) if args.empty?
|
122
|
+
instance_variable_set(ivar, *args)
|
123
|
+
self
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end # module Options
|
128
|
+
end # module Types
|
129
|
+
end # module Axiom
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Represents a symbol type
|
7
|
+
class Symbol < Object
|
8
|
+
extend LengthComparable, Encodable
|
9
|
+
|
10
|
+
primitive ::Symbol
|
11
|
+
coercion_method :to_symbol
|
12
|
+
|
13
|
+
minimum_length 0
|
14
|
+
maximum_length 255
|
15
|
+
|
16
|
+
end # class Symbol
|
17
|
+
end # module Types
|
18
|
+
end # module Axiom
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Represents a date time type
|
7
|
+
class Time < Object
|
8
|
+
extend ValueComparable
|
9
|
+
|
10
|
+
# The minimum and maximum seconds for Time on 32-bit systems
|
11
|
+
MINIMUM_SECONDS = -0x7FFFFFFF
|
12
|
+
MAXIMUM_SECONDS = 0x7FFFFFFF
|
13
|
+
|
14
|
+
primitive ::Time
|
15
|
+
coercion_method :to_time
|
16
|
+
|
17
|
+
minimum primitive.at(MINIMUM_SECONDS).utc
|
18
|
+
maximum primitive.at(MAXIMUM_SECONDS).utc
|
19
|
+
|
20
|
+
end # class Time
|
21
|
+
end # module Types
|
22
|
+
end # module Axiom
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Abstract base class for every type
|
7
|
+
class Type
|
8
|
+
extend Options, DescendantsTracker
|
9
|
+
|
10
|
+
accept_options :constraint
|
11
|
+
constraint Tautology
|
12
|
+
|
13
|
+
# Instantiate a new Axiom::Types::Type subclass
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# type = Axiom::Types::Type.new # => Axiom::Types::Type
|
17
|
+
#
|
18
|
+
# @param [#call] constraint
|
19
|
+
# optional constraint for the new type
|
20
|
+
#
|
21
|
+
# @yield [object]
|
22
|
+
#
|
23
|
+
# @yieldparam object [Object]
|
24
|
+
# test if the object matches the type constraint
|
25
|
+
#
|
26
|
+
# @yieldreturn [Boolean]
|
27
|
+
# true if the object matches the type constraint
|
28
|
+
#
|
29
|
+
# @return [Class<Axiom::Types::Type>]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def self.new(constraint = Undefined, &block)
|
33
|
+
type = ::Class.new(self, &block)
|
34
|
+
type.constraint(constraint)
|
35
|
+
type.finalize
|
36
|
+
end
|
37
|
+
|
38
|
+
# Finalize by deep freezing
|
39
|
+
#
|
40
|
+
# @return [Axiom::Types::Type]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def self.finalize
|
44
|
+
IceNine.deep_freeze(constraint)
|
45
|
+
freeze
|
46
|
+
end
|
47
|
+
|
48
|
+
# Test if the object matches the type constraint
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# type = Axiom::Types::Integer.new do
|
52
|
+
# minimum 1
|
53
|
+
# maximum 100
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# type.include?(1) # => true
|
57
|
+
# type.include?(100) # => true
|
58
|
+
# type.include?(0) # => false
|
59
|
+
# type.include?(101) # => false
|
60
|
+
#
|
61
|
+
# @param [Object] object
|
62
|
+
#
|
63
|
+
# @return [Boolean]
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def self.include?(object)
|
67
|
+
constraint.call(object)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Add a constraint to the type
|
71
|
+
#
|
72
|
+
# @example with an argument
|
73
|
+
# type.constraint(lambda { |object| object == 42 }
|
74
|
+
#
|
75
|
+
# @example with a block
|
76
|
+
# type.constraint { |object| object == 42 }
|
77
|
+
#
|
78
|
+
# @example with no arguments
|
79
|
+
# type.constraint # => constraint
|
80
|
+
#
|
81
|
+
# @param [#call] constraint
|
82
|
+
# optional constraint
|
83
|
+
#
|
84
|
+
# @yield [object]
|
85
|
+
#
|
86
|
+
# @yieldparam object [Object]
|
87
|
+
# test if the object matches the type constraint
|
88
|
+
#
|
89
|
+
# @yieldreturn [Boolean]
|
90
|
+
# true if the object matches the type constraint
|
91
|
+
#
|
92
|
+
# @return [self]
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def self.constraint(constraint = Undefined, &block)
|
96
|
+
constraint = block if constraint.equal?(Undefined)
|
97
|
+
return @constraint if constraint.nil?
|
98
|
+
add_constraint(constraint)
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
# Add a constraint that the object must be included in a set
|
103
|
+
#
|
104
|
+
# @param [Array<Object>] members
|
105
|
+
#
|
106
|
+
# @return [undefined]
|
107
|
+
#
|
108
|
+
# @todo move into a module
|
109
|
+
#
|
110
|
+
# @api private
|
111
|
+
def self.includes(*members)
|
112
|
+
set = IceNine.deep_freeze(members.to_set)
|
113
|
+
constraint(&set.method(:include?))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Add new constraint to existing constraint, if any
|
117
|
+
#
|
118
|
+
# @param [#call] constraint
|
119
|
+
#
|
120
|
+
# @return [undefined]
|
121
|
+
#
|
122
|
+
# @api private
|
123
|
+
def self.add_constraint(constraint)
|
124
|
+
current = self.constraint
|
125
|
+
@constraint = if current
|
126
|
+
lambda { |object| constraint.call(object) && current.call(object) }
|
127
|
+
else
|
128
|
+
constraint
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private_class_method :add_constraint
|
133
|
+
|
134
|
+
end # class Type
|
135
|
+
end # module Types
|
136
|
+
end # module Axiom
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Axiom
|
4
|
+
module Types
|
5
|
+
|
6
|
+
# Add a minimum and maximum value constraint to a type
|
7
|
+
module ValueComparable
|
8
|
+
|
9
|
+
# Hook called when module is extended
|
10
|
+
#
|
11
|
+
# Add #minimum and #maximum DSL methods to descendant.
|
12
|
+
#
|
13
|
+
# @param [Class<Axiom::Types::Type>] descendant
|
14
|
+
#
|
15
|
+
# @return [undefined]
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
def self.extended(descendant)
|
19
|
+
super
|
20
|
+
descendant.accept_options :minimum, :maximum
|
21
|
+
end
|
22
|
+
|
23
|
+
# Finalize by setting up a value range constraint
|
24
|
+
#
|
25
|
+
# @return [Axiom::Types::ValueComparable]
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
def finalize
|
29
|
+
return self if frozen?
|
30
|
+
has_value_within_range
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Add a constraint for a value within a range
|
37
|
+
#
|
38
|
+
# @return [undefined]
|
39
|
+
#
|
40
|
+
# @todo freeze the minimum and maximum
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def has_value_within_range
|
44
|
+
constraint { |object| object.between?(minimum, maximum) }
|
45
|
+
end
|
46
|
+
|
47
|
+
end # module ValueComparable
|
48
|
+
end # module Types
|
49
|
+
end # module Axiom
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'devtools'
|
4
|
+
|
5
|
+
Devtools.init_spec_helper
|
6
|
+
|
7
|
+
if ENV['COVERAGE'] == 'true'
|
8
|
+
require 'simplecov'
|
9
|
+
require 'coveralls'
|
10
|
+
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
12
|
+
SimpleCov::Formatter::HTMLFormatter,
|
13
|
+
Coveralls::SimpleCov::Formatter
|
14
|
+
]
|
15
|
+
|
16
|
+
SimpleCov.start do
|
17
|
+
command_name 'spec:unit'
|
18
|
+
add_filter 'config'
|
19
|
+
add_filter 'spec'
|
20
|
+
minimum_coverage 100
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'axiom-types'
|
25
|
+
|
26
|
+
include Axiom::Types
|
27
|
+
|
28
|
+
# require spec support files and shared behavior
|
29
|
+
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each do |file|
|
30
|
+
require file
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec.configure do |config|
|
34
|
+
config.expect_with :rspec do |expect_with|
|
35
|
+
expect_with.syntax = :expect
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Axiom::Types, '.finalize' do
|
6
|
+
subject { object.finalize }
|
7
|
+
|
8
|
+
let(:object) { described_class }
|
9
|
+
let(:descendants) { [ descendant ] }
|
10
|
+
let(:descendant) { mock('descendant').as_null_object }
|
11
|
+
|
12
|
+
before do
|
13
|
+
object::Type.should_receive(:descendants).and_return(descendants)
|
14
|
+
end
|
15
|
+
|
16
|
+
it_should_behave_like 'a command method'
|
17
|
+
|
18
|
+
it 'finalizes the descendants' do
|
19
|
+
descendant.should_receive(:finalize)
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
end
|