gorillib-model 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.
- 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
|