gorillib-model 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +12 -0
- data/README.md +21 -0
- data/Rakefile +15 -0
- data/gorillib-model.gemspec +27 -0
- data/lib/gorillib/builder.rb +239 -0
- data/lib/gorillib/core_ext/datetime.rb +23 -0
- data/lib/gorillib/core_ext/exception.rb +153 -0
- data/lib/gorillib/core_ext/module.rb +10 -0
- data/lib/gorillib/core_ext/object.rb +14 -0
- data/lib/gorillib/model/base.rb +273 -0
- data/lib/gorillib/model/collection/model_collection.rb +157 -0
- data/lib/gorillib/model/collection.rb +200 -0
- data/lib/gorillib/model/defaults.rb +115 -0
- data/lib/gorillib/model/errors.rb +24 -0
- data/lib/gorillib/model/factories.rb +555 -0
- data/lib/gorillib/model/field.rb +168 -0
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/named_schema.rb +53 -0
- data/lib/gorillib/model/positional_fields.rb +35 -0
- data/lib/gorillib/model/schema_magic.rb +163 -0
- data/lib/gorillib/model/serialization/csv.rb +60 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/to_wire.rb +54 -0
- data/lib/gorillib/model/serialization/tsv.rb +53 -0
- data/lib/gorillib/model/serialization.rb +41 -0
- data/lib/gorillib/model/type/extended.rb +83 -0
- data/lib/gorillib/model/type/ip_address.rb +153 -0
- data/lib/gorillib/model/type/url.rb +11 -0
- data/lib/gorillib/model/validate.rb +22 -0
- data/lib/gorillib/model/version.rb +5 -0
- data/lib/gorillib/model.rb +34 -0
- data/spec/builder_spec.rb +193 -0
- data/spec/core_ext/datetime_spec.rb +41 -0
- data/spec/core_ext/exception.rb +98 -0
- data/spec/core_ext/object.rb +45 -0
- data/spec/model/collection_spec.rb +290 -0
- data/spec/model/defaults_spec.rb +104 -0
- data/spec/model/factories_spec.rb +323 -0
- data/spec/model/lint_spec.rb +28 -0
- data/spec/model/serialization/csv_spec.rb +30 -0
- data/spec/model/serialization/tsv_spec.rb +28 -0
- data/spec/model/serialization_spec.rb +41 -0
- data/spec/model/type/extended_spec.rb +166 -0
- data/spec/model/type/ip_address_spec.rb +141 -0
- data/spec/model_spec.rb +261 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/capture_output.rb +28 -0
- data/spec/support/nuke_constants.rb +9 -0
- data/spec/support/shared_context_for_builders.rb +59 -0
- data/spec/support/shared_context_for_models.rb +55 -0
- data/spec/support/shared_examples_for_factories.rb +71 -0
- data/spec/support/shared_examples_for_model_fields.rb +62 -0
- data/spec/support/shared_examples_for_models.rb +87 -0
- metadata +193 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'serialization/to_wire'
|
2
|
+
require_relative 'serialization/lines'
|
3
|
+
require_relative 'serialization/csv'
|
4
|
+
require_relative 'serialization/tsv'
|
5
|
+
require_relative 'serialization/json'
|
6
|
+
|
7
|
+
class Array
|
8
|
+
def to_tsv
|
9
|
+
to_wire.join("\t")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Gorillib
|
14
|
+
module Model
|
15
|
+
|
16
|
+
def to_wire(options={})
|
17
|
+
compact_attributes.merge(:_type => self.class.typename).inject({}) do |acc, (key,attr)|
|
18
|
+
acc[key] = attr.respond_to?(:to_wire) ? attr.to_wire(options) : attr
|
19
|
+
acc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
def as_json(*args) to_wire(*args) ; end
|
23
|
+
|
24
|
+
def to_json(options={})
|
25
|
+
MultiJson.dump(to_wire(options), options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_tsv(options={})
|
29
|
+
attributes.map do |key, attr|
|
30
|
+
attr.respond_to?(:to_wire) ? attr.to_wire(options) : attr
|
31
|
+
end.join("\t")
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
def from_tuple(*vals)
|
36
|
+
receive Hash[field_names[0..vals.length-1].zip(vals)]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_relative 'ip_address'
|
2
|
+
require_relative 'url'
|
3
|
+
|
4
|
+
::Long = Class.new ::Integer
|
5
|
+
::Double = Class.new ::Float
|
6
|
+
::Binary = Class.new ::String
|
7
|
+
|
8
|
+
::Guid = Class.new ::String
|
9
|
+
::Hostname = Class.new ::String
|
10
|
+
|
11
|
+
::EpochTime = Class.new ::Integer
|
12
|
+
::IntTime = Class.new ::EpochTime
|
13
|
+
|
14
|
+
module Gorillib
|
15
|
+
module Factory
|
16
|
+
class GuidFactory < StringFactory ; self.product = ::Guid ; register_factory! ; end
|
17
|
+
class HostnameFactory < StringFactory ; self.product = ::Hostname ; register_factory! ; end
|
18
|
+
class IpAddressFactory < StringFactory ; self.product = ::IpAddress ; register_factory! ; end
|
19
|
+
|
20
|
+
class DateFactory < ConvertingFactory
|
21
|
+
self.product = Date
|
22
|
+
FLAT_DATE_RE = /\A\d{8}Z?\z/
|
23
|
+
register_factory!
|
24
|
+
#
|
25
|
+
def convert(obj)
|
26
|
+
case obj
|
27
|
+
when FLAT_DATE_RE then product.new(obj[0..3].to_i, obj[4..5].to_i, obj[6..7].to_i)
|
28
|
+
when Time then Date.new(obj.year, obj.month, obj.day)
|
29
|
+
when String then Date.parse(obj)
|
30
|
+
else mismatched!(obj)
|
31
|
+
end
|
32
|
+
rescue ArgumentError => err
|
33
|
+
raise if err.is_a?(TypeMismatchError)
|
34
|
+
warn "Cannot parse time #{obj}: #{err}"
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class EpochTimeFactory < ConvertingFactory
|
40
|
+
self.product = Integer
|
41
|
+
def self.typename() :epoch_time ; end
|
42
|
+
register_factory! :epoch_time, EpochTime
|
43
|
+
#
|
44
|
+
def convert(obj)
|
45
|
+
case obj
|
46
|
+
when Numeric then obj.to_f
|
47
|
+
when Time then obj.to_f
|
48
|
+
when /\A\d{14}Z?\z/ then Time.parse(obj)
|
49
|
+
when String then Time.parse_safely(obj).to_f
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class IntTimeFactory < EpochTimeFactory
|
55
|
+
def self.typename() :int_time ; end
|
56
|
+
register_factory! :int_time, IntTime
|
57
|
+
#
|
58
|
+
def convert(obj)
|
59
|
+
result = super
|
60
|
+
result.nil? ? nil : result.to_i
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Boolean10Factory < BooleanFactory
|
65
|
+
def self.typename() :boolean_10 ; end
|
66
|
+
register_factory! :boolean_10
|
67
|
+
#
|
68
|
+
def convert(obj)
|
69
|
+
case obj.to_s
|
70
|
+
when "0" then false
|
71
|
+
when "1" then true
|
72
|
+
else super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class SetFactory < EnumerableFactory
|
78
|
+
self.product = Set
|
79
|
+
register_factory!
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module IpAddresslike
|
2
|
+
ONES = 0xFFFFFFFF
|
3
|
+
|
4
|
+
# Masks off all but the `bitness` most-significant-bits
|
5
|
+
#
|
6
|
+
# @example /24 keeps only the first three quads
|
7
|
+
# IpAddress.new('1.2.3.4').bitness_min(24) # '1.2.3.0'
|
8
|
+
#
|
9
|
+
def bitness_min(bitness)
|
10
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
11
|
+
lsbs = 32 - bitness
|
12
|
+
(packed >> lsbs) << lsbs
|
13
|
+
end
|
14
|
+
|
15
|
+
# Masks off all but the `bitness` most-significant-bits, filling with ones
|
16
|
+
#
|
17
|
+
# @example /24 fills the last quad
|
18
|
+
# IpAddress.new('1.2.3.4').bitness_min(24) # '1.2.3.255'
|
19
|
+
#
|
20
|
+
def bitness_max(bitness)
|
21
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
22
|
+
packed | (ONES >> bitness)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hex
|
26
|
+
"%08x" % packed
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
dotted
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class ::IpAddress < ::String
|
36
|
+
include IpAddresslike
|
37
|
+
|
38
|
+
def dotted
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_i
|
43
|
+
packed
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Integer] the 32-bit integer for this IP address
|
47
|
+
def packed
|
48
|
+
ip_a, ip_b, ip_c, ip_d = quads
|
49
|
+
((ip_a << 24) + (ip_b << 16) + (ip_c << 8) + (ip_d))
|
50
|
+
end
|
51
|
+
|
52
|
+
def quads
|
53
|
+
self.split(".", 4).map{|qq| Integer(qq) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# === class methods ===
|
57
|
+
|
58
|
+
def self.from_packed(pi)
|
59
|
+
str = [ (pi >> 24) & 0xFF, (pi >> 16) & 0xFF, (pi >> 8) & 0xFF, (pi) & 0xFF ].join(".")
|
60
|
+
new(str)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from_dotted(str)
|
64
|
+
new(str)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Stores an IP address in numeric form.
|
69
|
+
#
|
70
|
+
# IpNumeric instances are immutable, and memoize most of their methods.
|
71
|
+
class ::IpNumeric
|
72
|
+
include IpAddresslike
|
73
|
+
include Comparable
|
74
|
+
|
75
|
+
def receive(val)
|
76
|
+
new(val)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(addr)
|
80
|
+
@packed = addr.to_int
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_i ; packed ; end
|
84
|
+
def to_int ; packed ; end
|
85
|
+
def ==(other) ; packed == other.to_int ; end
|
86
|
+
def <=>(other) ; packed <=> other.to_int ; end
|
87
|
+
def +(int) ; self.class.new(to_int + int) ; end
|
88
|
+
|
89
|
+
|
90
|
+
def packed ; @packed ; end
|
91
|
+
|
92
|
+
def dotted
|
93
|
+
@dotted ||= quads.join('.').freeze
|
94
|
+
end
|
95
|
+
|
96
|
+
def quads
|
97
|
+
@quads ||= [ (@packed >> 24) & 0xFF, (@packed >> 16) & 0xFF, (@packed >> 8) & 0xFF, (@packed) & 0xFF ].freeze
|
98
|
+
end
|
99
|
+
|
100
|
+
# === class methods ===
|
101
|
+
|
102
|
+
def self.from_packed(pi)
|
103
|
+
new(pi)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.from_dotted(dotted)
|
107
|
+
ip_a, ip_b, ip_c, ip_d = quads = dotted.split(".", 4).map(&:to_i)
|
108
|
+
obj = new((ip_a << 24) + (ip_b << 16) + (ip_c << 8) + (ip_d))
|
109
|
+
obj.instance_variable_set('@dotted', dotted.freeze)
|
110
|
+
obj.instance_variable_set('@quads', quads.freeze)
|
111
|
+
obj
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class ::IpRange < Range
|
116
|
+
|
117
|
+
def initialize(min_or_range, max=nil, exclusive=false)
|
118
|
+
if max.nil?
|
119
|
+
min = min_or_range.min
|
120
|
+
max = min_or_range.max
|
121
|
+
exclusive = min_or_range.exclude_end?
|
122
|
+
else
|
123
|
+
min = min_or_range
|
124
|
+
end
|
125
|
+
raise ArgumentError, "Only inclusive #{self.class.name}s are implemented" if exclusive
|
126
|
+
super( IpNumeric.new(min), IpNumeric.new(max), false )
|
127
|
+
end
|
128
|
+
|
129
|
+
def bitness_blocks(bitness)
|
130
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
131
|
+
return [] if min.nil?
|
132
|
+
lsbs = 32 - bitness
|
133
|
+
middle_min = min.bitness_max(bitness) + 1
|
134
|
+
return [[min, max]] if max < middle_min
|
135
|
+
middle_max = max.bitness_min(bitness)
|
136
|
+
blks = []
|
137
|
+
stride = 1 << lsbs
|
138
|
+
#
|
139
|
+
blks << [min, IpNumeric.new(middle_min-1)]
|
140
|
+
(middle_min ... middle_max).step(stride){|beg| blks << [IpNumeric.new(beg), IpNumeric.new(beg+stride-1)] }
|
141
|
+
blks << [IpNumeric.new(middle_max), max]
|
142
|
+
blks
|
143
|
+
end
|
144
|
+
|
145
|
+
CIDR_RE = %r{\A(\d+\.\d+\.\d+\.\d+)/([0-3]\d)\z}
|
146
|
+
|
147
|
+
def self.from_cidr(cidr_str)
|
148
|
+
cidr_str =~ CIDR_RE or raise ArgumentError, "CIDR string should look like an ip address and bitness, eg 1.2.3.4/24 (got #{cidr_str})"
|
149
|
+
bitness = $2.to_i
|
150
|
+
ip_address = IpNumeric.from_dotted($1)
|
151
|
+
new( ip_address.bitness_min(bitness), ip_address.bitness_max(bitness) )
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Gorillib
|
2
|
+
module Model
|
3
|
+
module Validate
|
4
|
+
module_function
|
5
|
+
|
6
|
+
VALID_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
7
|
+
def identifier!(name)
|
8
|
+
raise TypeError, "can't convert #{name.class} into Symbol", caller unless name.respond_to? :to_sym
|
9
|
+
raise ArgumentError, "Name must start with [A-Za-z_] and subsequently contain only [A-Za-z0-9_]", caller unless name =~ VALID_NAME_RE
|
10
|
+
end
|
11
|
+
|
12
|
+
def hashlike!(val)
|
13
|
+
return true if val.respond_to?(:[]) && val.respond_to?(:has_key?)
|
14
|
+
raise ArgumentError, "#{block_given? ? yield : 'value'} should be something that behaves like a hash: #{val.inspect}", caller
|
15
|
+
end
|
16
|
+
|
17
|
+
def included_in!(desc, val, colxn)
|
18
|
+
raise ArgumentError, "#{desc} must be one of #{colxn.inspect}: got #{val.inspect}", caller unless colxn.include?(val)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'time'
|
3
|
+
require 'date'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
require 'active_support/concern'
|
7
|
+
require 'active_support/core_ext/object/blank'
|
8
|
+
require 'active_support/core_ext/object/try'
|
9
|
+
require 'active_support/core_ext/array/extract_options'
|
10
|
+
require 'active_support/core_ext/class/attribute'
|
11
|
+
require 'active_support/core_ext/enumerable'
|
12
|
+
require 'active_support/core_ext/hash/compact'
|
13
|
+
require 'active_support/core_ext/hash/keys'
|
14
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
15
|
+
require 'active_support/core_ext/hash/slice'
|
16
|
+
require 'active_support/core_ext/module/delegation'
|
17
|
+
require 'active_support/inflector'
|
18
|
+
|
19
|
+
require 'gorillib/core_ext/datetime'
|
20
|
+
require 'gorillib/core_ext/exception'
|
21
|
+
require 'gorillib/core_ext/module'
|
22
|
+
require 'gorillib/core_ext/object'
|
23
|
+
|
24
|
+
require 'gorillib/model/version'
|
25
|
+
require 'gorillib/model/factories'
|
26
|
+
require 'gorillib/model/named_schema'
|
27
|
+
require 'gorillib/model/validate'
|
28
|
+
require 'gorillib/model/errors'
|
29
|
+
require 'gorillib/model/base'
|
30
|
+
require 'gorillib/model/schema_magic'
|
31
|
+
require 'gorillib/model/field'
|
32
|
+
require 'gorillib/model/defaults'
|
33
|
+
require 'gorillib/model/collection'
|
34
|
+
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'gorillib/builder'
|
4
|
+
|
5
|
+
describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
|
6
|
+
|
7
|
+
it_behaves_like 'a model'
|
8
|
+
|
9
|
+
context 'examples:' do
|
10
|
+
let(:subject_class ){ car_class }
|
11
|
+
it 'type-converts values' do
|
12
|
+
obj = subject_class.receive( :name => 'wildcat', :make_model => 'Buick Wildcat', :year => "1968", :doors => "2" )
|
13
|
+
obj.attributes.should == { :name => :wildcat, :make_model => 'Buick Wildcat', :year => 1968, :doors => 2, :engine => nil }
|
14
|
+
end
|
15
|
+
it 'handles nested structures' do
|
16
|
+
obj = subject_class.receive(
|
17
|
+
:name => 'wildcat', :make_model => 'Buick Wildcat', :year => "1968", :doors => "2",
|
18
|
+
:engine => { :carburetor => 'edelbrock', :volume => "455", :cylinders => '8' })
|
19
|
+
obj.attributes.values_at(:name, :make_model, :year, :doors).should == [:wildcat, 'Buick Wildcat', 1968, 2 ]
|
20
|
+
obj.engine.attributes.values_at(:carburetor, :volume, :cylinders).should == [:edelbrock, 455, 8 ]
|
21
|
+
end
|
22
|
+
it 'lets you dive down' do
|
23
|
+
wildcat.engine.attributes.values_at(:carburetor, :volume, :cylinders).should == [:stock, 455, 8 ]
|
24
|
+
wildcat.engine(:cylinders => 6) do
|
25
|
+
volume 383
|
26
|
+
end
|
27
|
+
wildcat.engine.attributes.values_at(:carburetor, :volume, :cylinders).should == [:stock, 383, 6 ]
|
28
|
+
end
|
29
|
+
it 'lazily autovivifies members' do
|
30
|
+
ford_39.read_attribute(:engine).should be_nil
|
31
|
+
ford_39.engine(:cylinders => 6)
|
32
|
+
ford_39.read_attribute(:engine).should be_a(Gorillib::Test::Engine)
|
33
|
+
ford_39.engine.read_attribute(:cylinders).should == 6
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'receive!' do
|
38
|
+
it 'with a block, instance evals the block' do
|
39
|
+
expect_7 = nil ; expect_obj = nil
|
40
|
+
wildcat.receive!({}){ expect_7 = 7 ; expect_obj = self }
|
41
|
+
expect_7.should == 7 ; expect_obj.should == wildcat
|
42
|
+
end
|
43
|
+
it 'with a block of arity 1, calls the block passing self' do
|
44
|
+
expect_7 = nil ; expect_obj = nil
|
45
|
+
wildcat.receive!({}){|c| expect_7 = 7 ; expect_obj = c }
|
46
|
+
expect_7.should == 7 ; expect_obj.should == wildcat
|
47
|
+
end
|
48
|
+
it 'with a block, returns its return value' do
|
49
|
+
val = mock_val
|
50
|
+
wildcat.receive!{ val }.should == val
|
51
|
+
wildcat.receive!{|obj| val }.should == val
|
52
|
+
end
|
53
|
+
it 'with no block, returns nil' do
|
54
|
+
wildcat.receive!.should be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context ".magic" do
|
59
|
+
let(:subject_class){ car_class }
|
60
|
+
context do
|
61
|
+
subject{ car_class.new }
|
62
|
+
let(:sample_val){ 'fiat' }
|
63
|
+
let(:raw_val ){ :fiat }
|
64
|
+
it_behaves_like "a model field", :make_model
|
65
|
+
it("#read_attribute is nil if never set"){ subject.read_attribute(:make_model).should == nil }
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not create a writer method #foo=" do
|
69
|
+
subject_class.should be_method_defined(:doors)
|
70
|
+
subject_class.should_not be_method_defined(:doors=)
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'calling the getset "#foo" method' do
|
74
|
+
subject{ wildcat }
|
75
|
+
|
76
|
+
it "with no args calls read_attribute(:foo)" do
|
77
|
+
subject.write_attribute(:doors, mock_val)
|
78
|
+
subject.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(mock_val)
|
79
|
+
subject.doors.should == mock_val
|
80
|
+
end
|
81
|
+
it "with an argument calls write_attribute(:foo)" do
|
82
|
+
subject.write_attribute(:doors, 'gone')
|
83
|
+
subject.should_receive(:write_attribute).with(:doors, mock_val).and_return('returned')
|
84
|
+
result = subject.doors(mock_val)
|
85
|
+
result.should == 'returned'
|
86
|
+
end
|
87
|
+
it "with multiple arguments is an error" do
|
88
|
+
expect{ subject.doors(1, 2) }.to raise_error(ArgumentError, "wrong number of arguments (2 for 0..1)")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context ".member" do
|
94
|
+
subject{ car_class.new }
|
95
|
+
let(:sample_val){ example_engine }
|
96
|
+
let(:raw_val ){ example_engine.attributes }
|
97
|
+
it_behaves_like "a model field", :engine
|
98
|
+
it("#read_attribute is nil if never set"){ subject.read_attribute(:engine).should == nil }
|
99
|
+
|
100
|
+
it "calling the getset method #foo with no args calls read_attribute(:foo)" do
|
101
|
+
wildcat.write_attribute(:doors, mock_val)
|
102
|
+
wildcat.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(mock_val)
|
103
|
+
wildcat.doors.should == mock_val
|
104
|
+
end
|
105
|
+
it "calling the getset method #foo with an argument calls write_attribute(:foo)" do
|
106
|
+
wildcat.write_attribute(:doors, 'gone')
|
107
|
+
wildcat.should_receive(:write_attribute).with(:doors, mock_val).and_return('returned')
|
108
|
+
result = wildcat.doors(mock_val)
|
109
|
+
result.should == 'returned'
|
110
|
+
end
|
111
|
+
it "calling the getset method #foo with multiple arguments is an error" do
|
112
|
+
->{ wildcat.doors(1, 2) }.should raise_error(ArgumentError, "wrong number of arguments (2 for 0..1)")
|
113
|
+
end
|
114
|
+
it "does not create a writer method #foo=" do
|
115
|
+
wildcat.should respond_to(:doors)
|
116
|
+
wildcat.should_not respond_to(:doors=)
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'collections' do
|
123
|
+
subject{ garage }
|
124
|
+
let(:sample_val){ Gorillib::ModelCollection.receive([wildcat], key_method: :name, item_type: car_class) }
|
125
|
+
let(:raw_val ){ [ wildcat.attributes ] }
|
126
|
+
it_behaves_like "a model field", :cars
|
127
|
+
it("#read_attribute is an empty collection if never set"){ subject.read_attribute(:cars).should == Gorillib::ModelCollection.new(key_method: :to_key) }
|
128
|
+
|
129
|
+
it 'a collection holds named objects' do
|
130
|
+
garage.cars.should be_empty
|
131
|
+
|
132
|
+
# create a car with a hash of attributes
|
133
|
+
garage.car(:cadzilla, :make_model => 'Cadillac, Mostly')
|
134
|
+
# ...and retrieve it by name
|
135
|
+
cadzilla = garage.car(:cadzilla)
|
136
|
+
|
137
|
+
# add a car explicitly
|
138
|
+
garage.car(:wildcat, wildcat)
|
139
|
+
garage.car(:wildcat).should equal(wildcat)
|
140
|
+
|
141
|
+
# duplicate a car
|
142
|
+
garage.car(:ford_39, ford_39.attributes.compact)
|
143
|
+
garage.car(:ford_39).should ==(ford_39)
|
144
|
+
garage.car(:ford_39).should_not equal(ford_39)
|
145
|
+
|
146
|
+
# examine the whole collection
|
147
|
+
garage.cars.keys.should == [:cadzilla, :wildcat, :ford_39]
|
148
|
+
garage.cars.should == Gorillib::ModelCollection.receive([cadzilla, wildcat, ford_39], key_method: :name, item_type: car_class)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'lazily autovivifies collection items' do
|
152
|
+
garage.cars.should be_empty
|
153
|
+
garage.car(:chimera).should be_a(car_class)
|
154
|
+
garage.cars.should == Gorillib::ModelCollection.receive([{:name => :chimera}], key_method: :name, item_type: car_class)
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'collection getset method' do
|
158
|
+
it 'clxn(:name, existing_object) -- replaces with given object, does not call block' do
|
159
|
+
test = nil
|
160
|
+
subject.car(:wildcat, wildcat).should equal(wildcat){ test = 3 }
|
161
|
+
test.should be_nil
|
162
|
+
end
|
163
|
+
it 'clxn(:name) (missing & no attributes given) -- autovivifies' do
|
164
|
+
subject.car(:cadzilla).should == Gorillib::Test::Car.new(:name => :cadzilla)
|
165
|
+
end
|
166
|
+
it 'clxn(:name, &block) (missing & no attributes given) -- autovivifies, execs block' do
|
167
|
+
test = nil
|
168
|
+
subject.car(:cadzilla){ test = 7 }
|
169
|
+
test.should == 7
|
170
|
+
end
|
171
|
+
it 'clxn(:name, :attr => val) (missing, attributes given) -- creates item' do
|
172
|
+
subject.car(:cadzilla, :doors => 3).should == Gorillib::Test::Car.new(:name => :cadzilla, :doors => 3)
|
173
|
+
end
|
174
|
+
it 'clxn(:name, :attr => val) (missing, attributes given) -- creates item, execs block' do
|
175
|
+
test = nil
|
176
|
+
subject.car(:cadzilla, :doors => 3){ test = 7 }
|
177
|
+
test.should == 7
|
178
|
+
end
|
179
|
+
it 'clxn(:name, :attr => val) (present, attributes given) -- updates item' do
|
180
|
+
subject.car(:wildcat, wildcat)
|
181
|
+
subject.car(:wildcat, :doors => 9)
|
182
|
+
wildcat.doors.should == 9
|
183
|
+
end
|
184
|
+
it 'clxn(:name, :attr => val) (present, attributes given) -- updates item, execs block' do
|
185
|
+
subject.car(:wildcat, wildcat)
|
186
|
+
subject.car(:wildcat, :doors => 9){ self.make_model 'WILDCAT' }
|
187
|
+
wildcat.doors.should == 9
|
188
|
+
wildcat.make_model.should == 'WILDCAT'
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Time, :datetime_spec => true do
|
4
|
+
describe '#parse_safely' do
|
5
|
+
before do
|
6
|
+
@time_utc = Time.parse("2011-02-03T04:05:06 UTC")
|
7
|
+
@time_cst = Time.parse("2011-02-02T22:05:06-06:00")
|
8
|
+
@time_flat = "20110203040506"
|
9
|
+
@time_iso_utc = "2011-02-03T04:05:06+00:00"
|
10
|
+
@time_iso_cst = "2011-02-02T22:05:06-06:00"
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'with a Time, passes it through.' do
|
14
|
+
Time.parse_safely(@time_utc).should == @time_utc
|
15
|
+
Time.parse_safely(@time_cst).should == @time_cst
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'with a Time, converts to UTC.' do
|
19
|
+
Time.parse_safely(@time_utc).utc_offset.should == 0
|
20
|
+
Time.parse_safely(@time_cst).utc_offset.should == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'with a flat time, converts to UTC Time instance' do
|
24
|
+
Time.parse_safely(@time_flat).should == @time_utc
|
25
|
+
Time.parse_safely(@time_flat).utc_offset.should == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'with a flat time and Z, converts to UTC Time instance' do
|
29
|
+
Time.parse_safely(@time_flat+'Z').should == @time_utc
|
30
|
+
Time.parse_safely(@time_flat+'Z').utc_offset.should == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'parses a regular time string, converting to UTC' do
|
34
|
+
Time.parse_safely(@time_iso_utc).should == @time_utc
|
35
|
+
Time.parse_safely(@time_iso_utc).utc_offset.should == 0
|
36
|
+
Time.parse_safely(@time_iso_cst).should == @time_utc
|
37
|
+
Time.parse_safely(@time_iso_cst).utc_offset.should == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'raisers' do
|
4
|
+
def should_raise_my_error(msg=nil)
|
5
|
+
msg ||= error_message
|
6
|
+
expect{ yield }.to raise_error(described_class, msg)
|
7
|
+
end
|
8
|
+
def should_return_true
|
9
|
+
yield.should be_true
|
10
|
+
end
|
11
|
+
# different rubies have different error messages ARRGH.
|
12
|
+
def capture_error
|
13
|
+
message = 'should have raised, did not'
|
14
|
+
begin
|
15
|
+
yield
|
16
|
+
rescue described_class => err
|
17
|
+
message = err.message
|
18
|
+
end
|
19
|
+
return message.gsub(/of arguments\(/, 'of arguments (')
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ArgumentError do
|
23
|
+
|
24
|
+
context '.check_arity!' do
|
25
|
+
let(:error_message){ /wrong number of arguments/ }
|
26
|
+
it 'checks against a range' do
|
27
|
+
should_raise_my_error{ described_class.check_arity!(['a'], 2..5) }
|
28
|
+
should_raise_my_error{ described_class.check_arity!(['a'], 2..2) }
|
29
|
+
should_return_true{ described_class.check_arity!(['a'], 0..5) }
|
30
|
+
should_return_true{ described_class.check_arity!(['a'], 1..1) }
|
31
|
+
end
|
32
|
+
it 'checks against an array' do
|
33
|
+
should_raise_my_error{ described_class.check_arity!( ['a', 'b'], [1, 3, 5] ) }
|
34
|
+
should_return_true{ described_class.check_arity!( ['a', 'b'], [1, 2] ) }
|
35
|
+
end
|
36
|
+
it 'given a single number, requires exactly that many args' do
|
37
|
+
should_raise_my_error{ described_class.check_arity!( ['a', 'b'], 1 ) }
|
38
|
+
should_raise_my_error{ described_class.check_arity!( ['a', 'b'], 3 ) }
|
39
|
+
should_return_true{ described_class.check_arity!( ['a', 'b'], 2 ) }
|
40
|
+
end
|
41
|
+
it 'matches the message a native arity error would' do
|
42
|
+
should_raise_my_error(capture_error{ [].fill() }){ described_class.check_arity!([], 1..3) }
|
43
|
+
should_raise_my_error(capture_error{ [].to_s(1) }){ described_class.check_arity!([1], 0) }
|
44
|
+
end
|
45
|
+
it 'appends result of block (if given) to message' do
|
46
|
+
str = "esiar no delave ylno"
|
47
|
+
->{ described_class.check_arity!([], 1..3){ str.reverse! } }.should raise_error(/only evaled on raise/)
|
48
|
+
str.should == "only evaled on raise"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context '.arity_at_least!' do
|
53
|
+
let(:error_message){ /wrong number of arguments/ }
|
54
|
+
it 'raises if there are fewer than that many args' do
|
55
|
+
should_raise_my_error{ described_class.arity_at_least!(['a'], 2) }
|
56
|
+
should_raise_my_error{ described_class.arity_at_least!([], 1) }
|
57
|
+
end
|
58
|
+
it ('returns true if there are that many args or more') do
|
59
|
+
should_return_true{ described_class.arity_at_least!([], 0) }
|
60
|
+
should_return_true{ described_class.arity_at_least!(['a'], 0) }
|
61
|
+
should_return_true{ described_class.arity_at_least!(['a'], 1) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe TypeMismatchError do
|
67
|
+
context '.mismatched!' do
|
68
|
+
let(:error_message){ /.+ has mismatched type/ }
|
69
|
+
it 'raises an error' do
|
70
|
+
should_raise_my_error{ described_class.mismatched!("string", Integer) }
|
71
|
+
should_raise_my_error{ described_class.mismatched!(Object.new) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context '.check_type!' do
|
76
|
+
let(:error_message){ /.+ has mismatched type; expected .+/ }
|
77
|
+
it 'raises true if any type matches' do
|
78
|
+
should_return_true{ described_class.check_type!("string", [Integer, String]) }
|
79
|
+
end
|
80
|
+
it 'raises an error if nothing matches' do
|
81
|
+
should_raise_my_error{ described_class.check_type!("string", [Integer, Float]) }
|
82
|
+
should_raise_my_error{ described_class.check_type!("string", [Integer]) }
|
83
|
+
should_raise_my_error{ described_class.check_type!("string", Integer) }
|
84
|
+
end
|
85
|
+
it 'checks is_a? given a class' do
|
86
|
+
should_return_true{ described_class.check_type!("string", [Integer, String]) }
|
87
|
+
should_return_true{ described_class.check_type!(7, [Integer, String]) }
|
88
|
+
should_raise_my_error{ described_class.check_type!(:symbol, [Integer, String]) }
|
89
|
+
end
|
90
|
+
it 'checks responds_to? given a symbol' do
|
91
|
+
should_return_true{ described_class.check_type!("string", [:to_str, :to_int]) }
|
92
|
+
should_return_true{ described_class.check_type!(7, [:to_str, :to_int]) }
|
93
|
+
should_raise_my_error{ described_class.check_type!(:symbol, [:to_str, :to_int]) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|