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.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +12 -0
  3. data/README.md +21 -0
  4. data/Rakefile +15 -0
  5. data/gorillib-model.gemspec +27 -0
  6. data/lib/gorillib/builder.rb +239 -0
  7. data/lib/gorillib/core_ext/datetime.rb +23 -0
  8. data/lib/gorillib/core_ext/exception.rb +153 -0
  9. data/lib/gorillib/core_ext/module.rb +10 -0
  10. data/lib/gorillib/core_ext/object.rb +14 -0
  11. data/lib/gorillib/model/base.rb +273 -0
  12. data/lib/gorillib/model/collection/model_collection.rb +157 -0
  13. data/lib/gorillib/model/collection.rb +200 -0
  14. data/lib/gorillib/model/defaults.rb +115 -0
  15. data/lib/gorillib/model/errors.rb +24 -0
  16. data/lib/gorillib/model/factories.rb +555 -0
  17. data/lib/gorillib/model/field.rb +168 -0
  18. data/lib/gorillib/model/lint.rb +24 -0
  19. data/lib/gorillib/model/named_schema.rb +53 -0
  20. data/lib/gorillib/model/positional_fields.rb +35 -0
  21. data/lib/gorillib/model/schema_magic.rb +163 -0
  22. data/lib/gorillib/model/serialization/csv.rb +60 -0
  23. data/lib/gorillib/model/serialization/json.rb +44 -0
  24. data/lib/gorillib/model/serialization/lines.rb +30 -0
  25. data/lib/gorillib/model/serialization/to_wire.rb +54 -0
  26. data/lib/gorillib/model/serialization/tsv.rb +53 -0
  27. data/lib/gorillib/model/serialization.rb +41 -0
  28. data/lib/gorillib/model/type/extended.rb +83 -0
  29. data/lib/gorillib/model/type/ip_address.rb +153 -0
  30. data/lib/gorillib/model/type/url.rb +11 -0
  31. data/lib/gorillib/model/validate.rb +22 -0
  32. data/lib/gorillib/model/version.rb +5 -0
  33. data/lib/gorillib/model.rb +34 -0
  34. data/spec/builder_spec.rb +193 -0
  35. data/spec/core_ext/datetime_spec.rb +41 -0
  36. data/spec/core_ext/exception.rb +98 -0
  37. data/spec/core_ext/object.rb +45 -0
  38. data/spec/model/collection_spec.rb +290 -0
  39. data/spec/model/defaults_spec.rb +104 -0
  40. data/spec/model/factories_spec.rb +323 -0
  41. data/spec/model/lint_spec.rb +28 -0
  42. data/spec/model/serialization/csv_spec.rb +30 -0
  43. data/spec/model/serialization/tsv_spec.rb +28 -0
  44. data/spec/model/serialization_spec.rb +41 -0
  45. data/spec/model/type/extended_spec.rb +166 -0
  46. data/spec/model/type/ip_address_spec.rb +141 -0
  47. data/spec/model_spec.rb +261 -0
  48. data/spec/spec_helper.rb +15 -0
  49. data/spec/support/capture_output.rb +28 -0
  50. data/spec/support/nuke_constants.rb +9 -0
  51. data/spec/support/shared_context_for_builders.rb +59 -0
  52. data/spec/support/shared_context_for_models.rb +55 -0
  53. data/spec/support/shared_examples_for_factories.rb +71 -0
  54. data/spec/support/shared_examples_for_model_fields.rb +62 -0
  55. data/spec/support/shared_examples_for_models.rb +87 -0
  56. 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,11 @@
1
+ require 'addressable/uri'
2
+
3
+ ::Url = Class.new Addressable::URI
4
+
5
+ module Gorillib::Factory
6
+ class UrlFactory < Gorillib::Factory::ConvertingFactory
7
+ self.product = ::Url
8
+ def convert(obj) product.parse(obj) ; end
9
+ register_factory!
10
+ end
11
+ 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,5 @@
1
+ module Gorillib
2
+ module Model
3
+ VERSION = '0.0.1'
4
+ end
5
+ 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