rrod 0.0.1 → 1.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/README.md +33 -18
  4. data/lib/rrod/all.rb +24 -1
  5. data/lib/rrod/caster/nested_model.rb +16 -0
  6. data/lib/rrod/caster.rb +34 -0
  7. data/lib/rrod/cli.rb +9 -2
  8. data/lib/rrod/configuration.rb +40 -0
  9. data/lib/rrod/model/attribute.rb +80 -0
  10. data/lib/rrod/model/attribute_methods.rb +85 -0
  11. data/lib/rrod/model/collection.rb +37 -0
  12. data/lib/rrod/model/finders.rb +59 -0
  13. data/lib/rrod/model/persistence.rb +35 -0
  14. data/lib/rrod/model/schema.rb +30 -0
  15. data/lib/rrod/model/serialization.rb +12 -0
  16. data/lib/rrod/model.rb +38 -0
  17. data/lib/rrod/query.rb +30 -0
  18. data/lib/rrod/test_server/rspec.rb +50 -0
  19. data/lib/rrod/test_server/runner.rb +56 -0
  20. data/lib/rrod/test_server.rb +77 -0
  21. data/lib/rrod/version.rb +2 -3
  22. data/rrod.gemspec +7 -1
  23. data/spec/rrod/caster_spec.rb +117 -0
  24. data/spec/rrod/configuration_spec.rb +55 -0
  25. data/spec/rrod/model/attribute_methods_spec.rb +64 -0
  26. data/spec/rrod/model/attribute_spec.rb +102 -0
  27. data/spec/rrod/model/collection_spec.rb +39 -0
  28. data/spec/rrod/model/finders_spec.rb +78 -0
  29. data/spec/rrod/model/persistence_spec.rb +43 -0
  30. data/spec/rrod/model/schema_spec.rb +59 -0
  31. data/spec/rrod/model/serialization_spec.rb +17 -0
  32. data/spec/rrod/model/validations_spec.rb +50 -0
  33. data/spec/rrod/model_spec.rb +50 -0
  34. data/spec/rrod/query_spec.rb +34 -0
  35. data/spec/rrod_spec.rb +4 -0
  36. data/spec/spec_helper.rb +4 -0
  37. data/spec/support/models/car.rb +3 -0
  38. data/spec/support/models/person.rb +41 -0
  39. data/spec/support/test_server.yml.example +17 -0
  40. metadata +106 -5
@@ -0,0 +1,50 @@
1
+ require 'rrod/test_server'
2
+
3
+ module Rrod
4
+ class TestServer
5
+ module RSpec
6
+
7
+ def self.enable!
8
+ Rrod.configure do |config|
9
+ # We have to use http to set bucket props (to enable search index)
10
+ # https://github.com/basho/riak-ruby-client/blob/v1.4.2/lib/riak/client.rb#L500
11
+ config.http_port = Rrod::TestServer.http_port
12
+ config.pb_port = Rrod::TestServer.pb_port
13
+ end
14
+
15
+ ::RSpec.configure do |config|
16
+ config.before(:each, :integration => true) do
17
+ if Rrod::TestServer.fatal
18
+ fail "Test server not working: #{Rrod::TestServer.fatal}"
19
+ end
20
+
21
+ if example.metadata[:test_server] == false
22
+ Rrod::TestServer.stop
23
+ else
24
+ Rrod::TestServer.create unless Rrod::TestServer.exist?
25
+ unless Rrod::TestServer.started?
26
+ @rspec_started = true
27
+ Rrod::TestServer.start
28
+ Rrod::TestServer.wait_for_search
29
+ end
30
+ end
31
+ end
32
+
33
+ config.after(:each, :integration => true) do
34
+ # i really don't understand this...
35
+ if !Rrod::TestServer.fatal &&
36
+ Rrod::TestServer.started? &&
37
+ example.metadata[:test_server] != false
38
+ Rrod::TestServer.drop
39
+ end
40
+ end
41
+
42
+ config.after(:suite) do
43
+ Rrod::TestServer.stop if @rspec_started && Rrod::TestServer.started?
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ require 'rrod/test_server'
2
+
3
+ module Rrod
4
+ class TestServer
5
+ class Runner
6
+
7
+ include Singleton
8
+
9
+ def self.run
10
+ instance.run
11
+ end
12
+
13
+ def self.signal_queue
14
+ @signal_queue ||= []
15
+ end
16
+
17
+ def run
18
+ define_signal_traps
19
+
20
+ Rrod::TestServer.tap { |server|
21
+ puts "starting rrod test server"
22
+ server.config[:root].tap { |root| FileUtils.rm_rf root if Dir.exists? root }
23
+ server.create unless server.exist?
24
+ server.start unless server.started?
25
+ begin puts "waiting for search..." end until server.search_started?
26
+ puts "started."
27
+ }
28
+
29
+ setup_run_loop
30
+ end
31
+
32
+ private
33
+
34
+ def setup_run_loop
35
+ loop do
36
+ case self.class.signal_queue.shift
37
+ when nil
38
+ sleep 1 # not really sure what to do here
39
+ when :INT, :TERM, :QUIT
40
+ puts
41
+ puts "shutting down rrod test server"
42
+ Rrod::TestServer.stop
43
+ break
44
+ end
45
+ end
46
+ end
47
+
48
+ def define_signal_traps
49
+ [:INT, :TERM, :QUIT].each do |signal|
50
+ Signal.trap(signal) { self.class.signal_queue << signal }
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,77 @@
1
+ # The test server was copied directly out of
2
+ # riak-ruby-client/spec/support/test_server.rb
3
+ # and refactored into a class that can be used
4
+ # to control the server's lifecycle
5
+
6
+ require 'yaml'
7
+ require 'riak/test_server'
8
+
9
+ module Rrod
10
+ class TestServer
11
+ attr_reader :fatal
12
+
13
+ extend Forwardable
14
+ include Singleton
15
+
16
+ DELEGATES = %w[wait_for_search start stop create drop http_port pb_port config started? search_started? exist?]
17
+
18
+ class << self
19
+ extend Forwardable
20
+ def_delegators(:instance, * [:fatal] + DELEGATES)
21
+ end
22
+
23
+ def_delegators(:server, *DELEGATES)
24
+
25
+ def config
26
+ loaded = YAML.load_file(Rrod.configuration.test_server_yml)
27
+ { min_port: 15_000 }.merge loaded.symbolize_keys
28
+ rescue Errno::ENOENT => e
29
+ message = "Cannot find Rrod::TestServer configuration. #{e.message}"
30
+ raise MissingConfigurationError.new(message)
31
+ end
32
+
33
+ def server
34
+ @server ||= try_creating_riak_test_server!
35
+ end
36
+
37
+ def search_started?
38
+ wait_for_search.chomp.eql? "riak_search is up"
39
+ end
40
+
41
+ def wait_for_search
42
+ Timeout.timeout(Rrod.configuration.test_server_search_startup_timeout) do
43
+ server.send :riak_admin, 'wait-for-service', 'riak_search', server.name
44
+ end
45
+ end
46
+
47
+ def warn_crash_log
48
+ if @server && crash_log = @server.log + 'crash.log'
49
+ warn crash_log.read if crash_log.exist?
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ attr_writer :fatal
56
+
57
+ private
58
+
59
+ # TODO refactor warnings
60
+ def try_creating_riak_test_server!
61
+ Riak::TestServer.create(config)
62
+ rescue SocketError => e
63
+ warn "Couldn't connect to Riak TestServer!"
64
+ warn "Skipping remaining integration tests."
65
+ warn_crash_log
66
+ self.fatal = e
67
+ rescue => e
68
+ warn "Can't run integration specs without the test server. Please create/verify spec/support/test_server.yml."
69
+ warn "Skipping remaining integration tests."
70
+ warn e.inspect
71
+ warn_crash_log
72
+ self.fatal = e
73
+ end
74
+
75
+ MissingConfigurationError = Class.new(StandardError)
76
+ end
77
+ end
data/lib/rrod/version.rb CHANGED
@@ -1,3 +1,2 @@
1
- module Rrod
2
- VERSION = "0.0.1"
3
- end
1
+ Rrod = Module.new unless defined? Rrod
2
+ Rrod::VERSION = '1.0.0.alpha.1'
data/rrod.gemspec CHANGED
@@ -18,11 +18,17 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "riak-client", "~> 1.4.2"
22
+ spec.add_dependency "activemodel", ">= 3.2"
23
+ spec.add_dependency "activesupport", ">= 3.2"
24
+ spec.add_dependency "american_date", ">= 1.1.0"
25
+
26
+ # cli deps
21
27
  spec.add_dependency "thor", "~> 0.18"
22
28
  spec.add_dependency "pry", "~> 0.9"
23
29
 
24
30
  spec.add_development_dependency "bundler", "~> 1.3"
25
- spec.add_development_dependency "rspec", "~> 2.14"
31
+ spec.add_development_dependency "rspec", "~> 2.14"
26
32
 
27
33
  spec.add_development_dependency "rake"
28
34
  end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rrod::Caster do
4
+ let(:type) { nil }
5
+ let(:caster) { "#{described_class}::#{type}".constantize }
6
+
7
+ describe "BigDecimal" do
8
+ let(:type) { 'BigDecimal' }
9
+ it "converts to big decimal" do
10
+ expect(caster.rrod_cast("42")).to eq 42
11
+ end
12
+ end
13
+
14
+ describe "Boolean" do
15
+ let(:type) { 'Boolean' }
16
+ it "converts to true" do
17
+ expect(caster.rrod_cast(true)).to be true
18
+ end
19
+ it "converts 'true' to true" do
20
+ expect(caster.rrod_cast("true")).to be true
21
+ end
22
+ it "converts 1 to true" do
23
+ expect(caster.rrod_cast(1)).to be true
24
+ end
25
+ it "converts '1' to true" do
26
+ expect(caster.rrod_cast("1")).to be true
27
+ end
28
+ it "converts to false" do
29
+ expect(caster.rrod_cast(false)).to be false
30
+ end
31
+ it "converts 'false' to false" do
32
+ expect(caster.rrod_cast("false")).to be false
33
+ end
34
+ it "converts 0 to false" do
35
+ expect(caster.rrod_cast(0)).to be false
36
+ end
37
+ it "converts '0' to false" do
38
+ expect(caster.rrod_cast("0")).to be false
39
+ end
40
+ it "converts nil to false" do
41
+ expect(caster.rrod_cast(nil)).to be false
42
+ end
43
+ it "converts everything else to true" do
44
+ expect(caster.rrod_cast(Object.new)).to be true
45
+ end
46
+
47
+ end
48
+
49
+ describe "Date" do
50
+ let(:type) { 'Date' }
51
+ let(:date) { Date.parse("2013-10-31") }
52
+ it "converts db format to date" do
53
+ expect(caster.rrod_cast("2013-10-31")).to eq date
54
+ end
55
+ it "converts standard format to date" do
56
+ expect(caster.rrod_cast("31-10-2013")).to eq date
57
+ end
58
+ it "converts american format to date" do
59
+ expect(caster.rrod_cast("10/31/2013")).to eq date
60
+ end
61
+ end
62
+
63
+ describe "DateTime" do
64
+ let(:type) { 'DateTime' }
65
+ let(:datetime) { "12:13:14".to_datetime }
66
+ it "converts time to datetime" do
67
+ expect(caster.rrod_cast("12:13:14")).to eq datetime
68
+ end
69
+ end
70
+
71
+ describe "Float" do
72
+ let(:type) { 'Float' }
73
+ let(:floater) { 3.142 }
74
+ it "converts to floating point" do
75
+ expect(caster.rrod_cast("3.142")).to eq floater
76
+ end
77
+ end
78
+
79
+ describe "Integer" do
80
+ let(:type) { 'Integer' }
81
+ it "converts to integer" do
82
+ expect(caster.rrod_cast('4')).to eq 4
83
+ end
84
+ end
85
+
86
+ describe "Numeric" do
87
+ let(:type) { 'Numeric' }
88
+ it "converts to floating point" do
89
+ expect(caster.rrod_cast("3.142")).to eq 3.142
90
+ end
91
+ it "converts to integer" do
92
+ expect(caster.rrod_cast("3")).to eq 3
93
+ end
94
+ end
95
+
96
+ describe "String" do
97
+ let(:type) { 'String' }
98
+ it "converts to string" do
99
+ expect(caster.rrod_cast(:string)).to eq 'string'
100
+ end
101
+ end
102
+
103
+ describe "Symbol" do
104
+ let(:type) { 'Symbol' }
105
+ it "converts to symbol" do
106
+ expect(caster.rrod_cast('symbol')).to eq :symbol
107
+ end
108
+ end
109
+
110
+ describe "Time" do
111
+ let(:type) { 'Time' }
112
+ let(:time) { "12:13:14".to_time }
113
+ it "converts to time" do
114
+ expect(caster.rrod_cast("12:13:14")).to eq time
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rrod do
4
+
5
+ it "adds a configure method to the Rrod module" do
6
+ expect(Rrod).to respond_to(:configure)
7
+ end
8
+
9
+ it "yields the configuration to the block given to config" do
10
+ Rrod.configure do |config|
11
+ config.pb_port = 123456
12
+ end
13
+ end
14
+
15
+ it "adds a configuration object to the Rrod module" do
16
+ expect(Rrod.configuration).to be_a Rrod::Configuration
17
+ end
18
+
19
+ end
20
+
21
+ describe Rrod::Configuration do
22
+
23
+ let(:config) { described_class.new }
24
+
25
+ describe "defaults" do
26
+ it "will use spec/support/test_server.yml for the test_server_yml" do
27
+ expect(config.test_server_yml).to match('spec/support/test_server.yml')
28
+ end
29
+ end
30
+
31
+ describe "client" do
32
+ it "creates a client based on its options" do
33
+ config.pb_port = 123456
34
+ expect(Riak::Client).to receive(:new).with(pb_port: 123456, protocol: 'pbc')
35
+ config.client
36
+ end
37
+
38
+ it "will not create a client if one exists" do
39
+ config.client = Riak::Client.new
40
+ expect(Riak::Client).not_to receive(:new)
41
+ config.client
42
+ end
43
+ end
44
+
45
+ describe "attributes" do
46
+ %w[http_port pb_port protocol client test_server_yml
47
+ test_server_search_startup_timeout nodes].each do |attribute|
48
+ it "allows configuration of #{attribute}" do
49
+ expect(config).to respond_to attribute
50
+ expect(config).to respond_to "#{attribute}="
51
+ end
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'support/models/car'
3
+
4
+ describe Rrod::Model do
5
+
6
+ let(:model) { Car }
7
+ let(:hash) { {wheels: 4, color: :black, make: 'Jeep'} }
8
+ let(:instance) { model.new(hash) }
9
+
10
+ describe "instantiation" do
11
+ it "can create an object with an arbitrary hash" do
12
+ expect(instance.wheels).to eq 4
13
+ expect(instance.color).to eq :black
14
+ expect(instance.make).to eq 'Jeep'
15
+ end
16
+
17
+ it "always has an id property" do
18
+ expect(instance).to respond_to :id
19
+ expect(instance.id).to be_nil
20
+ end
21
+
22
+ it "manages attribute keys as strings" do
23
+ expect(instance.attributes).to eq hash.stringify_keys
24
+ end
25
+
26
+ it "ignores modifications to the attribute hash" do
27
+ instance.attributes[:model] = 'Wrangler'
28
+ expect(instance.attributes[:model]).to be_nil
29
+ end
30
+
31
+ it "will return nil for an attribute that exists in the hash but does not have a corresponding method" do
32
+ instance.instance_variable_get(:@attributes)['foo'] = 'bar'
33
+ expect(instance).not_to respond_to(:foo)
34
+ expect(instance.attributes).to include('foo' => nil)
35
+ end
36
+
37
+ describe "mass assignment" do
38
+ it "will merge attributes when mass assigning" do
39
+ instance.attributes = {wheels: 5}
40
+ expect(instance.wheels).to eq 5
41
+ end
42
+
43
+ it "will not add additional attribute methods after instantiation" do
44
+ expect { instance.attributes = {model: 'Wrangler'} }.to raise_error
45
+ end
46
+ end
47
+
48
+ describe "with schema" do
49
+ let(:model) { Class.new(Car) { attribute :wheels, Integer } }
50
+
51
+ it "does not allow creating with arbitrary attributes" do
52
+ expect { model.new(model: 'Jeep') }.to raise_error(NoMethodError, /model=/)
53
+ end
54
+
55
+ it "can be created with the specified attributes" do
56
+ expect(model.new(wheels: 5).wheels).to be 5
57
+ end
58
+ end
59
+
60
+ describe "query generation" do
61
+ it "lets you use strings with apostrophes in them"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rrod::Model::Attribute do
4
+ let(:options) { {presence: true} }
5
+ let(:model) { Class.new { include Rrod::Model } }
6
+ let(:instance) { model.new }
7
+ let(:name) { :name }
8
+ let(:type) { String }
9
+ let(:attribute) { described_class.new(model, name, type, options) }
10
+
11
+ it "has a model" do
12
+ expect(attribute.model).to be model
13
+ end
14
+
15
+ it "has a name" do
16
+ expect(attribute.name).to be name
17
+ end
18
+
19
+ it "typecasts name to a symbol when initializing" do
20
+ attribute = described_class.new(model, "unicorn_tears", Integer)
21
+ expect(attribute.name).to be :unicorn_tears
22
+ end
23
+
24
+ it "has a type" do
25
+ expect(attribute.type).to be type
26
+ end
27
+
28
+ it "has options" do
29
+ expect(attribute.options).to eq(options)
30
+ end
31
+
32
+ describe "defaults" do
33
+ context "when a value" do
34
+ let(:options) { {default: 'SOO fluffy!'} }
35
+ it "can provide a default value" do
36
+ expect(attribute.default).to eq 'SOO fluffy!'
37
+ end
38
+ end
39
+
40
+ context "when a proc" do
41
+ let(:options) { {default: -> { 'alligator' }} }
42
+
43
+ it "can provide a default value if a proc" do
44
+ expect(attribute.default).to eq 'alligator'
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "defining attribute methods" do
50
+ before(:each) { attribute.define }
51
+
52
+ describe "getters" do
53
+ it "is defined on the declaring class" do
54
+ expect(instance).to respond_to(name)
55
+ end
56
+ end
57
+
58
+ describe "setters" do
59
+ it "is defined on the declaring class" do
60
+ expect(instance).to respond_to("#{name}=")
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "casting" do
66
+ it "will use the types rrod_cast method if available" do
67
+ type = Class.new {
68
+ def self.rrod_cast(value)
69
+ value.to_s.upcase
70
+ end
71
+ }
72
+ model.attribute :example, type
73
+ instance.example = 'shorty'
74
+ expect(instance.example).to eq 'SHORTY'
75
+ end
76
+
77
+ it "will look for a register Rrod caster if the type cannot cast itself" do
78
+ model.attribute :example, String
79
+ instance.example = :whazzup
80
+ expect(instance.example).to eq('whazzup')
81
+ end
82
+
83
+ it "will simply return the value if all else fails" do
84
+ model.attribute :example, Class.new
85
+ instance.example = self
86
+ expect(instance.example).to be self
87
+ end
88
+
89
+ describe "nested rrod models" do
90
+ let(:type) { [Class.new { include Rrod::Model }] }
91
+
92
+ it "can detect if the type is a nested rrod model" do
93
+ expect(attribute.send :nested_model?).to be true
94
+ end
95
+
96
+ it "adds a rrod_cast method to the nested rrod model reference object" do
97
+ attribute
98
+ expect(type).to respond_to(:rrod_cast)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'support/models/person'
3
+
4
+ describe Rrod::Model::Collection do
5
+ let(:array) { [Pet.new] }
6
+ let(:collection) { described_class.new(array) }
7
+
8
+ describe "initialization" do
9
+ it "takes a collection" do
10
+ expect(collection.collection).to eq(array)
11
+ end
12
+
13
+ it "defaults to an empty collection" do
14
+ expect(described_class.new.collection).to be_an Array
15
+ end
16
+
17
+ describe "errors" do
18
+ describe "non enumerable" do
19
+ let(:array) { Object.new }
20
+ it "raises" do
21
+ expect { collection }.to raise_error(Rrod::Model::Collection::InvalidCollectionTypeError)
22
+ end
23
+ end
24
+
25
+ describe "not all Rrod::Model" do
26
+ let(:array) { [Pet.new, Object.new] }
27
+ it "raises if not all `Rrod::Model`s" do
28
+ expect { collection }.to raise_error(Rrod::Model::Collection::InvalidMemberTypeError)
29
+ end
30
+
31
+ it "clears the collection" do
32
+ collection = described_class.new
33
+ begin; collection.collection = array; rescue; end
34
+ expect(collection.collection).to be_empty
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+ require 'support/models/car'
3
+
4
+ describe Rrod::Model::Finders, integration: true do
5
+
6
+ let(:model) { Car }
7
+ let(:hash) { {wheels: 4, color: :black, make: 'Jeep'} }
8
+ let(:instance) { model.new(hash) }
9
+
10
+ before :each do
11
+ instance.save
12
+ end
13
+
14
+ it "can be found by id" do
15
+ found = model.find(instance.id)
16
+ expect(found).to be_a model
17
+ end
18
+
19
+ it "will raise an error if it can't be found" do
20
+ expect { model.find("id that is not there") }.to raise_error(Rrod::Model::NotFound)
21
+ end
22
+
23
+ it "is persisted" do
24
+ found = model.find(instance.id)
25
+ expect(found).to be_persisted
26
+ end
27
+
28
+ it "sets the robject on the found instance" do
29
+ found = model.find(instance.id)
30
+ expect(found.robject).to be_a Riak::RObject
31
+ end
32
+
33
+ describe "finding by attributes in the hash" do
34
+ describe "find_first_by" do
35
+ it "can find one" do
36
+ found = model.find_first_by(make: 'Jeep')
37
+ expect(found).to be_a model
38
+ expect(found.make).to eq "Jeep"
39
+ end
40
+
41
+ it "will work properly when finding by id" do
42
+ found = model.find_first_by(id: instance.id)
43
+ expect(found).to be_a model
44
+ end
45
+
46
+ it "will return nil if one can't be found" do
47
+ found = model.find_first_by(id: "id that is not there")
48
+ expect(found).to be nil
49
+ end
50
+
51
+ it "will raise an exception if one can't be found with a !" do
52
+ expect { model.find_first_by! zombies: true }.to raise_error(Rrod::Model::NotFound)
53
+ end
54
+ end
55
+
56
+ describe "find_all_by" do
57
+ it "can find all" do
58
+ founds = model.find_all_by(make: 'Jeep', wheels: 4)
59
+ found = founds.first
60
+ expect(founds).to be_an Array
61
+ expect(found).to be_a model
62
+ expect(found.make).to eq "Jeep"
63
+ end
64
+
65
+ it "will raise an exception if finding all by id" do
66
+ expect {model.find_all_by(id: instance.id)}.to raise_error(ArgumentError)
67
+ end
68
+
69
+ it "will return [] if none can be found" do
70
+ expect(model.find_all_by zombies: 'yes plz').to eq []
71
+ end
72
+
73
+ it "will raise an exception if none can be found with a !" do
74
+ expect { model.find_all_by! brains: :none }.to raise_error(Rrod::Model::NotFound)
75
+ end
76
+ end
77
+ end
78
+ end