attributor 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -0
  3. data/.travis.yml +6 -4
  4. data/CHANGELOG.md +6 -1
  5. data/Gemfile +1 -1
  6. data/Guardfile +14 -8
  7. data/Rakefile +4 -5
  8. data/attributor.gemspec +34 -29
  9. data/lib/attributor.rb +23 -29
  10. data/lib/attributor/attribute.rb +108 -127
  11. data/lib/attributor/attribute_resolver.rb +12 -26
  12. data/lib/attributor/dsl_compiler.rb +17 -21
  13. data/lib/attributor/dumpable.rb +1 -2
  14. data/lib/attributor/example_mixin.rb +5 -8
  15. data/lib/attributor/exceptions.rb +5 -6
  16. data/lib/attributor/extensions/randexp.rb +3 -5
  17. data/lib/attributor/extras/field_selector.rb +4 -4
  18. data/lib/attributor/extras/field_selector/transformer.rb +6 -7
  19. data/lib/attributor/families/numeric.rb +0 -2
  20. data/lib/attributor/families/temporal.rb +1 -4
  21. data/lib/attributor/hash_dsl_compiler.rb +22 -25
  22. data/lib/attributor/type.rb +24 -32
  23. data/lib/attributor/types/bigdecimal.rb +7 -14
  24. data/lib/attributor/types/boolean.rb +5 -8
  25. data/lib/attributor/types/class.rb +9 -10
  26. data/lib/attributor/types/collection.rb +34 -44
  27. data/lib/attributor/types/container.rb +9 -15
  28. data/lib/attributor/types/csv.rb +7 -10
  29. data/lib/attributor/types/date.rb +20 -25
  30. data/lib/attributor/types/date_time.rb +7 -14
  31. data/lib/attributor/types/float.rb +4 -6
  32. data/lib/attributor/types/hash.rb +171 -196
  33. data/lib/attributor/types/ids.rb +2 -6
  34. data/lib/attributor/types/integer.rb +12 -17
  35. data/lib/attributor/types/model.rb +39 -48
  36. data/lib/attributor/types/object.rb +2 -4
  37. data/lib/attributor/types/polymorphic.rb +118 -0
  38. data/lib/attributor/types/regexp.rb +4 -5
  39. data/lib/attributor/types/string.rb +6 -7
  40. data/lib/attributor/types/struct.rb +8 -15
  41. data/lib/attributor/types/symbol.rb +3 -6
  42. data/lib/attributor/types/tempfile.rb +5 -6
  43. data/lib/attributor/types/time.rb +11 -11
  44. data/lib/attributor/types/uri.rb +9 -10
  45. data/lib/attributor/version.rb +1 -1
  46. data/spec/attribute_resolver_spec.rb +57 -78
  47. data/spec/attribute_spec.rb +174 -216
  48. data/spec/attributor_spec.rb +11 -15
  49. data/spec/dsl_compiler_spec.rb +19 -33
  50. data/spec/dumpable_spec.rb +6 -7
  51. data/spec/extras/field_selector/field_selector_spec.rb +1 -1
  52. data/spec/families_spec.rb +1 -3
  53. data/spec/hash_dsl_compiler_spec.rb +65 -74
  54. data/spec/spec_helper.rb +9 -3
  55. data/spec/support/hashes.rb +2 -3
  56. data/spec/support/models.rb +30 -36
  57. data/spec/support/polymorphics.rb +10 -0
  58. data/spec/type_spec.rb +38 -61
  59. data/spec/types/bigdecimal_spec.rb +11 -15
  60. data/spec/types/boolean_spec.rb +12 -39
  61. data/spec/types/class_spec.rb +10 -11
  62. data/spec/types/collection_spec.rb +72 -81
  63. data/spec/types/container_spec.rb +22 -26
  64. data/spec/types/csv_spec.rb +15 -16
  65. data/spec/types/date_spec.rb +16 -33
  66. data/spec/types/date_time_spec.rb +16 -33
  67. data/spec/types/file_upload_spec.rb +1 -2
  68. data/spec/types/float_spec.rb +7 -14
  69. data/spec/types/hash_spec.rb +285 -289
  70. data/spec/types/ids_spec.rb +5 -7
  71. data/spec/types/integer_spec.rb +37 -46
  72. data/spec/types/model_spec.rb +111 -128
  73. data/spec/types/polymorphic_spec.rb +134 -0
  74. data/spec/types/regexp_spec.rb +4 -7
  75. data/spec/types/string_spec.rb +17 -21
  76. data/spec/types/struct_spec.rb +40 -47
  77. data/spec/types/tempfile_spec.rb +1 -2
  78. data/spec/types/temporal_spec.rb +9 -0
  79. data/spec/types/time_spec.rb +16 -32
  80. data/spec/types/type_spec.rb +15 -0
  81. data/spec/types/uri_spec.rb +6 -7
  82. metadata +77 -25
@@ -11,8 +11,10 @@ Coveralls.wear! do
11
11
  end
12
12
 
13
13
  require 'rspec'
14
- require 'attributor'
14
+ require 'rspec/its'
15
+ require 'rspec/collection_matchers'
15
16
 
17
+ require 'attributor'
16
18
  require 'pry'
17
19
 
18
20
  # Requires supporting files with custom matchers and macros, etc,
@@ -20,13 +22,11 @@ require 'pry'
20
22
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
21
23
 
22
24
  RSpec.configure do |config|
23
-
24
25
  config.around(:each) do |example|
25
26
  Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
26
27
  example.run
27
28
  Attributor::AttributeResolver.current = nil
28
29
  end
29
-
30
30
  end
31
31
 
32
32
  RSpec::Matchers.define :be_in_family do |expected|
@@ -34,3 +34,9 @@ RSpec::Matchers.define :be_in_family do |expected|
34
34
  actual.family == expected
35
35
  end
36
36
  end
37
+
38
+ RSpec::Matchers.define :be_subclass_of do |expected|
39
+ match do |actual|
40
+ actual < expected
41
+ end
42
+ end
@@ -1,14 +1,13 @@
1
1
  class HashWithModel < Attributor::Hash
2
2
  keys do
3
- key :name, String, :default => "Turkey McDucken", :description => "Turducken name", :example => /[:name:]/
3
+ key :name, String, default: 'Turkey McDucken', description: 'Turducken name', example: Randgen.name
4
4
  key :chicken, Chicken
5
5
  end
6
6
  end
7
7
 
8
-
9
8
  class HashWithStrings < Attributor::Hash
10
9
  keys do
11
10
  key :name, String
12
11
  key :something, String
13
12
  end
14
- end
13
+ end
@@ -1,42 +1,41 @@
1
1
  class Chicken < Attributor::Model
2
- attributes(:identity => :email) do
3
- attribute :name, Attributor::String, example: /[:first_name:]/
4
- attribute :age, Attributor::Integer, :default => 1, :min => 0, :max => 120, :description => "The age of the chicken"
5
- attribute :email, Attributor::String, :example => /[:email:]/, :regexp => /@/, :description => "The email address of the chicken"
6
- attribute :angry, Attributor::Boolean, :example => "true", :description => "Angry bird?"
7
- attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :description => "The weight of the chicken"
2
+ attributes(identity: :email) do
3
+ attribute :name, Attributor::String, example: Randgen.first_name
4
+ attribute :age, Attributor::Integer, default: 1, min: 0, max: 120, description: 'The age of the chicken'
5
+ attribute :email, Attributor::String, example: Randgen.email, regexp: /@/, description: 'The email address of the chicken'
6
+ attribute :angry, Attributor::Boolean, example: 'true', description: 'Angry bird?'
7
+ attribute :weight, Attributor::Float, example: /\d{1,2}\.\d/, description: 'The weight of the chicken'
8
+ attribute :type, Attributor::Symbol, values: [:chicken]
8
9
  end
9
10
  end
10
11
 
11
-
12
12
  class Duck < Attributor::Model
13
13
  attributes do
14
- attribute :age, Attributor::Integer, :required_if => {"name" => "Daffy" }
14
+ attribute :age, Attributor::Integer, required_if: { 'name' => 'Daffy' }
15
15
  attribute :name, Attributor::String
16
- attribute :email, Attributor::String, :required_if => "name"
17
- attribute :angry, Attributor::Boolean, :default => true, :example => /true|false/, :description => "Angry bird?"
18
- attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :description => "The weight of the duck"
16
+ attribute :email, Attributor::String, required_if: 'name'
17
+ attribute :angry, Attributor::Boolean, default: true, example: /true|false/, description: 'Angry bird?'
18
+ attribute :weight, Attributor::Float, example: /\d{1,2}\.\d/, description: 'The weight of the duck'
19
+ attribute :type, Attributor::Symbol, values: [:duck]
19
20
  end
20
21
  end
21
22
 
22
-
23
23
  class Turkey < Attributor::Model
24
24
  attributes do
25
- attribute :age, Integer, :default => 1, :min => 0, :max => 120, :description => "The age of the turkey"
26
- attribute :name, String , :description => "name of the turkey", :example => /[:name:]/ #, :default => "Providencia Zboncak"
27
- attribute :email, String, :example => /[:email:]/, :regexp => /@/, :description => "The email address of the turkey"
28
- attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d/, :max => 86.7, :description => "The weight of the turkey"
25
+ attribute :age, Integer, default: 1, min: 0, max: 120, description: 'The age of the turkey'
26
+ attribute :name, String, description: 'name of the turkey', example: Randgen.name # , :default => "Providencia Zboncak"
27
+ attribute :email, String, example: Randgen.email, regexp: /@/, description: 'The email address of the turkey'
28
+ attribute :weight, Attributor::Float, example: /\d{1,2}\.\d/, max: 100.0, description: 'The weight of the turkey'
29
+ attribute :type, Attributor::Symbol, values: [:turkey]
29
30
  end
30
31
  end
31
32
 
32
-
33
-
34
33
  class Turducken < Attributor::Model
35
34
  attributes do
36
- attribute :name, String, :default => "Turkey McDucken", :description => "Turducken name", :example => /[:name:]/
35
+ attribute :name, String, default: 'Turkey McDucken', description: 'Turducken name', example: Randgen.name
37
36
  attribute :chicken, Chicken
38
37
  attribute :duck, Duck
39
- attribute :turkey, Turkey, :description => "The turkey"
38
+ attribute :turkey, Turkey, description: 'The turkey'
40
39
  end
41
40
  end
42
41
 
@@ -44,54 +43,49 @@ end
44
43
 
45
44
  class Cormorant < Attributor::Model
46
45
  attributes do
47
- attribute :name, String, :description => "Name of the Cormorant", :example => /[:name:]/
46
+ attribute :name, String, description: 'Name of the Cormorant', example: Randgen.name
48
47
  attribute :timestamps do
49
48
  attribute :born_at, DateTime
50
- attribute :died_at, DateTime, example: Proc.new {|timestamps| timestamps.born_at + 10}
49
+ attribute :died_at, DateTime, example: proc { |timestamps| timestamps.born_at + 10 }
51
50
  end
52
51
 
53
52
  # This will be a collection of arbitrary Ruby Objects
54
- attribute :fish, Attributor::Collection, :description => "All kinds of fish for feeding the babies"
53
+ attribute :fish, Attributor::Collection, description: 'All kinds of fish for feeding the babies'
55
54
 
56
55
  # This will be a collection of Cormorants (note, this relationship is circular)
57
- attribute :neighbors, Attributor::Collection.of(Cormorant), :description => "Neighbor cormorants"
56
+ attribute :neighbors, Attributor::Collection.of(Cormorant), description: 'Neighbor cormorants'
58
57
 
59
58
  # This will be a collection of instances of an anonymous Struct class, each having two well-defined attributes
60
59
 
61
- attribute :babies, Attributor::Collection.of(Attributor::Struct), :description => "All the babies", :member_options => {:identity => :name} do
62
- attribute :name, Attributor::String, :example => /[:name]/, :description => "The name of the baby cormorant"
63
- attribute :months, Attributor::Integer, :default => 0, :min => 0, :description => "The age in months of the baby cormorant"
64
- attribute :weight, Attributor::Float, :example => /\d{1,2}\.\d{3}/, :description => "The weight in kg of the baby cormorant"
60
+ attribute :babies, Attributor::Collection.of(Attributor::Struct), description: 'All the babies', member_options: { identity: :name } do
61
+ attribute :name, Attributor::String, example: /[:name]/, description: 'The name of the baby cormorant'
62
+ attribute :months, Attributor::Integer, default: 0, min: 0, description: 'The age in months of the baby cormorant'
63
+ attribute :weight, Attributor::Float, example: /\d{1,2}\.\d{3}/, description: 'The weight in kg of the baby cormorant'
65
64
  end
66
-
67
65
  end
68
66
  end
69
67
 
70
-
71
68
  class Person < Attributor::Model
72
69
  attributes do
73
- attribute :name, String, example: /[:first_name:]/
74
- attribute :title, String, values: %w{Mr Mrs Ms Dr}
70
+ attribute :name, String, example: Randgen.first_name
71
+ attribute :title, String, values: %w(Mr Mrs Ms Dr)
75
72
  attribute :okay, Attributor::Boolean, values: [true]
76
73
  attribute :address, Address, example: proc { |person, context| Address.example(context, person: person) }
77
74
  end
78
75
  end
79
76
 
80
-
81
77
  class Address < Attributor::Model
82
78
  attributes do
83
79
  attribute :name, String, example: /\w+/
84
- attribute :state, String, values: %w{OR CA}
80
+ attribute :state, String, values: %w(OR CA)
85
81
  attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
86
82
  requires :name
87
83
  end
88
84
  end
89
85
 
90
-
91
86
  class Post < Attributor::Model
92
87
  attributes do
93
88
  attribute :title, String
94
89
  attribute :tags, Attributor::Collection.of(String)
95
90
  end
96
91
  end
97
-
@@ -0,0 +1,10 @@
1
+ class Sandwich < Attributor::Model
2
+ attributes do
3
+ attribute :name, String
4
+ attribute :meat, Attributor::Polymorphic.on(:type) do
5
+ given :chicken, Chicken
6
+ given :turkey, Turkey
7
+ given :duck, Duck
8
+ end
9
+ end
10
+ end
@@ -1,8 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
2
 
3
-
4
3
  describe Attributor::Type do
5
-
6
4
  subject(:test_type) do
7
5
  Class.new do
8
6
  include Attributor::Type
@@ -24,14 +22,13 @@ describe Attributor::Type do
24
22
  let(:attribute_attributes) { Hash.new }
25
23
 
26
24
  let(:attribute) do
27
- double "attribute",
28
- :options => attribute_options,
29
- :attributes => attribute_attributes
25
+ double 'attribute',
26
+ options: attribute_options,
27
+ attributes: attribute_attributes
30
28
  end
31
29
 
32
-
33
30
  its(:native_type) { should be(::String) }
34
- its(:id) { should eq('Testing')}
31
+ its(:id) { should eq('Testing') }
35
32
 
36
33
  context 'anonymous' do
37
34
  its(:anonymous?) { should be(false) }
@@ -39,13 +36,13 @@ describe Attributor::Type do
39
36
  klass = Class.new do
40
37
  include Attributor::Type
41
38
  end
42
- expect( klass.anonymous? ).to be(true)
39
+ expect(klass.anonymous?).to be(true)
43
40
  end
44
41
  it 'can be set to true explicitly' do
45
42
  klass = Class.new(test_type) do
46
43
  anonymous_type
47
44
  end
48
- expect( klass.anonymous? ).to be(true)
45
+ expect(klass.anonymous?).to be(true)
49
46
  end
50
47
  end
51
48
 
@@ -53,80 +50,69 @@ describe Attributor::Type do
53
50
  let(:value) { nil }
54
51
  let(:context) { nil }
55
52
 
56
- context "when given a nil value" do
53
+ context 'when given a nil value' do
57
54
  it 'always successfully returns it (i.e., you can always load nil)' do
58
- test_type.load(value).should be(value)
55
+ expect(test_type.load(value)).to be(value)
59
56
  end
60
57
  end
61
58
 
62
- context "when given a value that is of native_type" do
63
- let(:value) { "one" }
59
+ context 'when given a value that is of native_type' do
60
+ let(:value) { 'one' }
64
61
  it 'returns the value' do
65
- test_type.load(value).should be(value)
62
+ expect(test_type.load(value)).to be(value)
66
63
  end
67
64
  end
68
65
 
69
- context "when given a value that is not of native_type" do
66
+ context 'when given a value that is not of native_type' do
70
67
  let(:value) { 1 }
71
- let(:context) { ['top','sub'] }
68
+ let(:context) { %w(top sub) }
72
69
 
73
70
  it 'raises an exception' do
74
- expect { test_type.load(value,context) }.to raise_error( Attributor::IncompatibleTypeError, /cannot load values of type Fixnum.*while loading top.sub/)
71
+ expect { test_type.load(value, context) }.to raise_error(Attributor::IncompatibleTypeError, /cannot load values of type Fixnum.*while loading top.sub/)
75
72
  end
76
-
77
-
78
73
  end
79
-
80
74
  end
81
75
 
82
-
83
76
  context 'validate' do
84
77
  let(:context) { ['some_attribute'] }
85
78
 
86
79
  let(:attribute_options) { {} }
87
80
 
88
- let(:attribute) { double("some_attribute", :options => attribute_options)}
89
- subject(:errors) { test_type.validate(value, context, attribute) }
81
+ let(:attribute) { double('some_attribute', options: attribute_options) }
82
+ subject(:errors) { test_type.validate(value, context, attribute) }
90
83
 
91
84
  context 'min and max' do
92
85
  let(:min) { 10 }
93
- let(:max) { 100}
86
+ let(:max) { 100 }
94
87
 
95
- let(:attribute_options) { {:min => min, :max => max} }
88
+ let(:attribute_options) { { min: min, max: max } }
96
89
 
97
-
98
-
99
- context "with a value <= min" do
90
+ context 'with a value <= min' do
100
91
  let(:value) { 1 }
101
92
 
102
93
  it { should_not be_empty }
103
94
  it 'returns the correct error message' do
104
- errors.first.should =~ /value \(#{value}\) is smaller than the allowed min/
95
+ expect(errors.first).to match(/value \(#{value}\) is smaller than the allowed min/)
105
96
  end
106
97
  end
107
98
 
108
- context "with a value >= max" do
99
+ context 'with a value >= max' do
109
100
  let(:value) { 1000 }
110
101
  it { should_not be_empty }
111
102
  it 'returns the correct error message' do
112
- errors.first.should =~ /value \(#{value}\) is larger than the allowed max/
103
+ expect(errors.first).to match(/value \(#{value}\) is larger than the allowed max/)
113
104
  end
114
-
115
105
  end
116
106
 
117
107
  context 'with a value within the range' do
118
108
  let(:value) { 50 }
119
109
  it { should be_empty }
120
110
  end
121
-
122
-
123
111
  end
124
112
 
125
-
126
-
127
- context 'regexp' do
113
+ context 'regexp' do
128
114
  let(:regexp) { /dog/ }
129
- let(:attribute_options) { {:regexp => regexp} }
115
+ let(:attribute_options) { { regexp: regexp } }
130
116
 
131
117
  context 'with a value that matches' do
132
118
  let(:value) { 'bulldog' }
@@ -134,68 +120,59 @@ describe Attributor::Type do
134
120
  it { should be_empty }
135
121
  end
136
122
 
137
-
138
123
  context 'with a value that does not match' do
139
124
  let(:value) { 'chicken' }
140
- it { should_not be_empty}
125
+ it { should_not be_empty }
141
126
  it 'returns the correct error message' do
142
- errors.first.should =~ /value \(#{value}\) does not match regexp/
127
+ expect(errors.first).to match(/value \(#{value}\) does not match regexp/)
143
128
  end
144
129
  end
145
-
146
- end
147
-
148
-
130
+ end
149
131
  end
150
132
 
151
-
152
133
  context 'example' do
153
-
154
134
  end
155
135
 
156
136
  context 'id' do
157
137
  it 'works for built-in types' do
158
- Attributor::String.id.should eq('Attributor-String')
138
+ expect(Attributor::String.id).to eq('Attributor-String')
159
139
  end
160
140
 
161
141
  it 'returns nil for anonymous types' do
162
142
  type = Class.new(Attributor::Model)
163
- type.id.should eq(nil)
143
+ expect(type.id).to eq(nil)
164
144
  end
165
145
  end
166
146
 
167
147
  context 'describe' do
168
- let(:example){ "Foo" }
148
+ let(:example) { 'Foo' }
169
149
  subject(:description) { test_type.describe }
170
150
  it 'outputs the type name' do
171
- description[:name].should eq(test_type.name)
151
+ expect(description[:name]).to eq(test_type.name)
172
152
  end
173
153
  it 'outputs the type id' do
174
- description[:id].should eq(test_type.name)
154
+ expect(description[:id]).to eq(test_type.name)
175
155
  end
176
156
 
177
157
  context 'with an example' do
178
158
  subject(:description) { test_type.describe(example: example) }
179
159
  it 'includes it in the :example key' do
180
- description.should have_key(:example)
181
- description[:example].should be(example)
160
+ expect(description).to have_key(:example)
161
+ expect(description[:example]).to be(example)
182
162
  end
183
163
  end
184
164
 
185
165
  context 'when anonymous' do
186
-
187
166
  it 'reports true in the output when set (to true default)' do
188
167
  anon_type = Class.new(test_type) { anonymous_type }
189
- anon_type.describe.should have_key(:anonymous)
190
- anon_type.describe[:anonymous].should be(true)
168
+ expect(anon_type.describe).to have_key(:anonymous)
169
+ expect(anon_type.describe[:anonymous]).to be(true)
191
170
  end
192
171
  it 'reports false in the output when set false explicitly' do
193
172
  anon_type = Class.new(test_type) { anonymous_type false }
194
- anon_type.describe.should have_key(:anonymous)
195
- anon_type.describe[:anonymous].should be(false)
173
+ expect(anon_type.describe).to have_key(:anonymous)
174
+ expect(anon_type.describe[:anonymous]).to be(false)
196
175
  end
197
-
198
176
  end
199
177
  end
200
-
201
178
  end
@@ -4,7 +4,7 @@ describe Attributor::BigDecimal do
4
4
  subject(:type) { Attributor::BigDecimal }
5
5
 
6
6
  it 'it is not Dumpable' do
7
- type.new.is_a?(Attributor::Dumpable).should_not be(true)
7
+ expect(type.new.is_a?(Attributor::Dumpable)).not_to be(true)
8
8
  end
9
9
 
10
10
  context '.native_type' do
@@ -13,40 +13,36 @@ describe Attributor::BigDecimal do
13
13
 
14
14
  context '.example' do
15
15
  its(:example) { should be_a(::BigDecimal) }
16
- it do
17
- ex = type.example
18
- end
19
16
  end
20
17
 
21
18
  context '.load' do
22
19
  let(:value) { nil }
23
20
  it 'returns nil for nil' do
24
- type.load(nil).should be(nil)
21
+ expect(type.load(nil)).to be(nil)
25
22
  end
26
23
 
27
24
  context 'for incoming Float values' do
28
- it 'returns the incoming value' do
29
- [0.0, -1.0, 1.0, 1e-10, 0.25135].each do |value|
30
- type.load(value).should eq(value)
31
- end
32
- end
25
+ it 'returns the incoming value' do
26
+ [0.0, -1.0, 1.0, 1e-10, 0.25135].each do |value|
27
+ expect(type.load(value)).to eq(value)
28
+ end
29
+ end
33
30
  end
34
31
 
35
32
  context 'for incoming Integer values' do
36
33
  it 'should equal the incoming value' do
37
34
  [0, -1, 1].each do |value|
38
- type.load(value).should eq(value)
35
+ expect(type.load(value)).to eq(value)
39
36
  end
40
37
  end
41
38
  end
42
39
 
43
40
  context 'for incoming String values' do
44
41
  it 'should equal the value' do
45
- type.load('0').should eq(0)
46
- type.load('100').should eq(100)
47
- type.load('0.1').should eq(0.1)
42
+ expect(type.load('0')).to eq(0)
43
+ expect(type.load('100')).to eq(100)
44
+ expect(type.load('0.1')).to eq(0.1)
48
45
  end
49
46
  end
50
-
51
47
  end
52
48
  end