gorillib-model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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