couchmodel 0.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +156 -0
  3. data/Rakefile +20 -0
  4. data/lib/core_extension/array.rb +14 -0
  5. data/lib/core_extension/string.rb +12 -0
  6. data/lib/couch_model/active_model.rb +86 -0
  7. data/lib/couch_model/base/accessor.rb +39 -0
  8. data/lib/couch_model/base/association.rb +63 -0
  9. data/lib/couch_model/base/finder.rb +28 -0
  10. data/lib/couch_model/base/setup.rb +88 -0
  11. data/lib/couch_model/base.rb +117 -0
  12. data/lib/couch_model/collection.rb +84 -0
  13. data/lib/couch_model/configuration.rb +68 -0
  14. data/lib/couch_model/database.rb +64 -0
  15. data/lib/couch_model/design.rb +92 -0
  16. data/lib/couch_model/server.rb +44 -0
  17. data/lib/couch_model/transport.rb +68 -0
  18. data/lib/couch_model/view.rb +52 -0
  19. data/lib/couch_model.rb +15 -0
  20. data/spec/fake_transport.yml +202 -0
  21. data/spec/fake_transport_helper.rb +27 -0
  22. data/spec/integration/basic_spec.rb +125 -0
  23. data/spec/integration/design/membership.design +5 -0
  24. data/spec/integration/design/user.design +2 -0
  25. data/spec/lib/core_extension/array_spec.rb +24 -0
  26. data/spec/lib/core_extension/string_spec.rb +22 -0
  27. data/spec/lib/couch_model/active_model_spec.rb +228 -0
  28. data/spec/lib/couch_model/base_spec.rb +169 -0
  29. data/spec/lib/couch_model/collection_spec.rb +100 -0
  30. data/spec/lib/couch_model/configuration_spec.rb +117 -0
  31. data/spec/lib/couch_model/core/accessor_spec.rb +59 -0
  32. data/spec/lib/couch_model/core/association_spec.rb +114 -0
  33. data/spec/lib/couch_model/core/finder_spec.rb +24 -0
  34. data/spec/lib/couch_model/core/setup_spec.rb +88 -0
  35. data/spec/lib/couch_model/database_spec.rb +165 -0
  36. data/spec/lib/couch_model/design/association_test_model_one.design +5 -0
  37. data/spec/lib/couch_model/design/base_test_model.design +10 -0
  38. data/spec/lib/couch_model/design/setup_test_model.design +10 -0
  39. data/spec/lib/couch_model/design_spec.rb +144 -0
  40. data/spec/lib/couch_model/server_spec.rb +64 -0
  41. data/spec/lib/couch_model/transport_spec.rb +44 -0
  42. data/spec/lib/couch_model/view_spec.rb +166 -0
  43. data/spec/lib/couch_model_spec.rb +3 -0
  44. data/spec/spec_helper.rb +27 -0
  45. metadata +128 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Philipp Brüll
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,156 @@
1
+
2
+ = CouchModel
3
+
4
+ The intent of CouchModel is, to provide an easy interface handle CouchDB documents. It also comes with a ActiveModel
5
+ implementation to integrate into an Rails 3 application.
6
+
7
+ The current version is under development and open for everyone to find bugs and post them into the issue tracker.
8
+
9
+ The code has been tested Ruby 1.9.1, CouchDB 0.10.0 and Rails 3.0.0.beta.
10
+
11
+ == Dependencies
12
+
13
+ If CouchModel is used without Rails, the ruby standard library (tested with 1.9.1) if the only requirement.
14
+
15
+ If the activemodel gem is installed, CouchModel automatically provides an interface to Rails 3.
16
+
17
+ To run the test suite, <tt>rspec</tt> (tested with 1.2.9) is required. A CouchDB instance is just required for the
18
+ integration tests (task <tt>spec:integration</tt>).
19
+
20
+ == Installation
21
+
22
+ The gem is part of the gemcutter archive. It can be installed by simply type
23
+
24
+ gem install couchmodel
25
+
26
+ == Defining a model
27
+
28
+ To define a model, it's necessary to create a subclass of <tt>CouchModel::Base</tt>
29
+
30
+ class User < CouchModel::Base
31
+
32
+ setup_database :url => "http://localhost:5984/test",
33
+ :setup_on_initialization => true,
34
+ :delete_if_exists => false
35
+
36
+ key_accessor :name
37
+ key_accessor :email
38
+
39
+ end
40
+
41
+ The <tt>setup_database</tt> method defines a database for the model. The +url+ option is required and specifies the url
42
+ of the database in the scheme <em>http://[host]:[port]/[database_name]</em>. If the option
43
+ <tt>setup_on_initialization</tt> is set to true, CouchModel will try to create the database when the model is
44
+ initialized. If the option <tt>delete_if_exists</tt> is specified, the database will be deleted and re-created. If the
45
+ option <tt>setup_on_initialization</tt> is not specified or false, the database setup be done manually by calling
46
+ <tt>CouchModel::Configuration.setup_databases</tt> and <tt>CouchModel::Configuration.setup_designs</tt>.
47
+
48
+ The method <tt>key_accessor</tt> defined access methods to the given keys of the CouchDB document. It's also possible
49
+ to use <tt>key_reader</tt> and <tt>key_writer</tt> here.
50
+
51
+ == Design documents
52
+
53
+ Each defined model has a realted design document, that keeps all the views for that model. Via the command
54
+
55
+ CouchModel::Configuration.design_directory = "[directory]"
56
+
57
+ a directory is specfied that keeps all the design document. CouchModel will watch out for a file with the name
58
+ <em>[design directory]/[model_name].design</em> and will use it as the related design document. If no such file exists,
59
+ a design document will be created (but not saved to the file). The design ducument can be asscessed via
60
+ <tt>Model.design</tt>.
61
+
62
+ A design document should look like this
63
+
64
+ :id: "test_design"
65
+ :language: "javascript"
66
+ :views:
67
+ "view_name_1":
68
+ :map:
69
+ function(document) {
70
+ ...
71
+ };
72
+ :reduce:
73
+ function(key, values, rereduce) {
74
+ ...
75
+ };
76
+ "view_name_2":
77
+ :keys: [ "key_one", "key_two" ]
78
+ ...
79
+
80
+ It will create the methods <tt>Model.view_name_1</tt> and <tt>Model.view_name_2</tt>, which returns the result of the
81
+ related view. It's also possible to pass some extra options like <tt>startkey</tt> or <tt>key</tt> to these methods.
82
+
83
+ The view can be defined by write down a map and a reduce function or provide the <tt>keys</tt> array. If the
84
+ <tt>keys</tt> array is given, CouchModel will generate a map function that emits the given array of document keys. The
85
+ reduce function will be set to null.
86
+
87
+ CouchModel also creates by default a class view. This view simply selects all documents from the corresponding model
88
+ and is assigned to the method <tt>Model.all</tt>.
89
+
90
+ == Associations
91
+
92
+ CouchModel provides support for simple association definition. Currently, the method <tt>belongs_to</tt> and
93
+ <tt>has_many</tt> are implmented.
94
+
95
+ class User < CouchModel::Base
96
+
97
+ ...
98
+
99
+ belongs_to :session, :class_name => "UserSession"
100
+
101
+ has_many :memberships,
102
+ :class_name => "Membership",
103
+ :view_name => :by_user_id_and_created_at
104
+ :query => proc { |created_at| { :startkey => [ self.id, (create_at || nil) ], :endkey => [ self.id, (created_at || { }) ] } }
105
+
106
+ end
107
+
108
+ In this example, the <tt>belongs_to</tt> adds a <tt>key_accessor</tt> named <tt>session_id</tt> to the user and also
109
+ generates getters and setters for the session object itself (<tt>session</tt> and <tt>session=</tt>).
110
+
111
+ The <tt>has_many</tt> acts as a wrapper for the specified view. The previously defined view
112
+ <em>by_user_id_and_created_at</em> emits membership-documents by thier <tt>user_id</tt> and the <tt>created_at</tt>
113
+ date. The given query option specifes a method that returns a query hash for the specifed view. The arguments for this
114
+ method can be passed membership association method.
115
+
116
+ user.membership(created_at)
117
+
118
+ The returned <tt>CouchModel::Collection</tt> can be treated as an (read-only) array.
119
+
120
+ == Rails integration
121
+
122
+ The following steps has been tested with the first beta version of Rails 3 (activemodel-3.0.0.beta).
123
+
124
+ First of all, the <tt>couchmodel</tt> gem has to added to the dependencies. This can be done by adding
125
+
126
+ gem "couchmodel", :require => "couch_model"
127
+
128
+ to the <tt>Gemfile</tt>.
129
+
130
+ The configuration can be done by creating an initializer. Here is an example file
131
+ (e.g. <tt>config/initializer/couch_model.rb</tt>).
132
+
133
+ CouchModel::Configuration.design_directory = File.join(Rails.root, "app", "models", "designs")
134
+
135
+ DATABASE = {
136
+ :test => { :url => "http://localhost:5984/test", :setup_on_initialization => true, :delete_if_exists => true }
137
+ :development => { :url => "http://localhost:5984/development", :setup_on_initialization => true, :delete_if_exists => false },
138
+ :production => { :url => "http://localhost:5984/production", :setup_on_initialization => true, :delete_if_exists => false }
139
+ }[Rails.env.to_sym] unless defined?(DATABASE)
140
+
141
+ This example uses an sub-directory of <tt>app/models</tt> to search for the design documents. It also defined a constant
142
+ named <tt>DATABASE</tt> that is initialized with the right database setup for the each environment. This constant can
143
+ then be used to define the models.
144
+
145
+ class User < CouchModel::Base
146
+
147
+ setup_database DATABASE
148
+
149
+ ...
150
+
151
+ end
152
+
153
+ == Development
154
+
155
+ CouchModel is still under development and needs to be tested. Any contribution is welcome!
156
+
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ gem 'rspec'
3
+ require 'spec'
4
+ require 'spec/rake/spectask'
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs in spec directory"
9
+ Spec::Rake::SpecTask.new do |task|
10
+ task.spec_files = FileList["spec/lib/**/*_spec.rb"]
11
+ end
12
+
13
+ namespace :spec do
14
+
15
+ desc "Run all integration specs in spec/integration directory"
16
+ Spec::Rake::SpecTask.new(:integration) do |task|
17
+ task.spec_files = FileList["spec/integration/**/*_spec.rb"]
18
+ end
19
+
20
+ end
@@ -0,0 +1,14 @@
1
+
2
+ class Array
3
+
4
+ def self.wrap(object)
5
+ if object.nil?
6
+ []
7
+ elsif object.respond_to?(:to_ary)
8
+ object.to_ary
9
+ else
10
+ [object]
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,12 @@
1
+
2
+ class String
3
+
4
+ def underscore
5
+ self.gsub(/([a-z][A-Z])/){ |match| "#{match[0]}_#{match[1]}" }.downcase
6
+ end
7
+
8
+ def camelize
9
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
10
+ end
11
+
12
+ end
@@ -0,0 +1,86 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "core_extension", "array"))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "base"))
3
+ gem 'activemodel'
4
+ require 'active_model'
5
+
6
+ module CouchModel
7
+
8
+ class Base
9
+ extend ::ActiveModel::Naming
10
+ extend ::ActiveModel::Callbacks
11
+ extend ::ActiveModel::Translation
12
+ include ::ActiveModel::Conversion
13
+ include ::ActiveModel::Dirty
14
+ include ::ActiveModel::Validations
15
+ include ::ActiveModel::Serializers::JSON
16
+ include ::ActiveModel::Serializers::Xml
17
+
18
+ CALLBACKS = [ :initialize, :save, :create, :update, :destroy ].freeze unless defined?(CALLBACKS)
19
+
20
+ define_model_callbacks *CALLBACKS
21
+
22
+ CALLBACKS.each do |method_name|
23
+
24
+ alias :"#{method_name}_without_callbacks" :"#{method_name}"
25
+
26
+ define_method :"#{method_name}" do |*arguments|
27
+ send :"_run_#{method_name}_callbacks" do
28
+ send :"#{method_name}_without_callbacks", *arguments
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ alias new_record? new?
35
+
36
+ alias destroyed? new?
37
+
38
+ alias save_without_dirty save
39
+
40
+ def save
41
+ result = save_without_dirty
42
+ discard_changes!
43
+ result
44
+ end
45
+
46
+ private
47
+
48
+ def discard_changes!
49
+ @previously_changed = changes
50
+ @changed_attributes = { }
51
+ end
52
+
53
+ class << self
54
+
55
+ alias key_accessor_without_dirty key_accessor
56
+
57
+ def key_accessor(key)
58
+ add_key key
59
+ redefine_attribute_methods
60
+
61
+ key_accessor_without_dirty key
62
+
63
+ alias_method :"#{key}_without_dirty=", :"#{key}="
64
+ define_method :"#{key}=" do |value|
65
+ send :"#{key}_will_change!"
66
+ send :"#{key}_without_dirty=", value
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def add_key(key)
73
+ @keys ||= [ ]
74
+ @keys << key
75
+ end
76
+
77
+ def redefine_attribute_methods
78
+ undefine_attribute_methods
79
+ define_attribute_methods @keys
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,39 @@
1
+
2
+ module CouchModel
3
+
4
+ class Base
5
+
6
+ module Accessor
7
+
8
+ def self.included(base_class)
9
+ base_class.class_eval do
10
+ extend ClassMethods
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def key_reader(key)
17
+ define_method :"#{key}" do
18
+ @attributes[key.to_s]
19
+ end
20
+ end
21
+
22
+ def key_writer(key)
23
+ define_method :"#{key}=" do |value|
24
+ @attributes[key.to_s] = value
25
+ end
26
+ end
27
+
28
+ def key_accessor(key)
29
+ key_reader key
30
+ key_writer key
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,63 @@
1
+
2
+ module CouchModel
3
+
4
+ class Base
5
+
6
+ module Association
7
+
8
+ def self.included(base_class)
9
+ base_class.class_eval do
10
+ extend ClassMethods
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def belongs_to(name, options = { })
17
+ class_name = options[:class_name] || name.to_s.camelize
18
+ key = options[:key] || "#{name}_id"
19
+
20
+ key_accessor key
21
+
22
+ define_method :"#{name}" do
23
+ klass = Object.const_get class_name
24
+ klass.find self.send(key)
25
+ end
26
+
27
+ define_method :"#{name}=" do |value|
28
+ klass = Object.const_get class_name
29
+ if value
30
+ raise ArgumentError, "only objects of class #{klass} are accepted" unless value.is_a?(klass)
31
+ self.send :"#{key}=", value.id
32
+ else
33
+ self.send :"#{key}=", nil
34
+ end
35
+ end
36
+ end
37
+
38
+ def has_many(name, options = { })
39
+ class_name = options[:class_name] || name.to_s.camelize
40
+ view_name = options[:view_name] || raise(ArgumentError, "no view_name is given")
41
+ query = options[:query]
42
+
43
+ define_method :"#{name}_query", &query if query.is_a?(Proc)
44
+
45
+ define_method :"#{name}" do |*arguments|
46
+ klass = Object.const_get class_name
47
+ query = if self.respond_to?(:"#{name}_query")
48
+ arguments << nil while arguments.length < self.method(:"#{name}_query").arity
49
+ self.send :"#{name}_query", *arguments
50
+ else
51
+ { :key => "\"#{self.id}\"" }
52
+ end
53
+ klass.send :"#{view_name}", query
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,28 @@
1
+
2
+ module CouchModel
3
+
4
+ class Base
5
+
6
+ module Finder
7
+
8
+ def self.included(base_class)
9
+ base_class.class_eval do
10
+ extend ClassMethods
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def find(id)
17
+ document = new :id => id
18
+ document.load
19
+ document
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,88 @@
1
+
2
+ module CouchModel
3
+
4
+ class Base
5
+
6
+ module Setup
7
+
8
+ def self.included(base_class)
9
+ base_class.class_eval do
10
+ include InstanceMethods
11
+ extend ClassMethods
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+
17
+ def database
18
+ self.class.database
19
+ end
20
+
21
+ end
22
+
23
+ module ClassMethods
24
+
25
+ def setup_database(options = { })
26
+ initialize_database options
27
+ initialize_design
28
+ generate_class_view
29
+ push_design options
30
+ end
31
+
32
+ def database
33
+ @database || raise(StandardError, "no database defined!")
34
+ end
35
+
36
+ def design
37
+ @design || raise(StandardError, "no database defined!")
38
+ end
39
+
40
+ def method_missing(method_name, *arguments, &block)
41
+ view = find_view method_name
42
+ view ? view.collection(*arguments) : super
43
+ end
44
+
45
+ def respond_to?(method_name)
46
+ view = find_view method_name
47
+ view ? true : super
48
+ end
49
+
50
+ private
51
+
52
+ def initialize_database(options)
53
+ url = options[:url] || raise(ArgumentError, "no url was given to define the database")
54
+ setup_on_initialization = options[:setup_on_initialization] || false
55
+
56
+ uri = URI.parse url
57
+ server = Server.new :host => uri.host, :port => uri.port
58
+ database = Database.new :server => server, :name => uri.path.gsub("/", "")
59
+ @database = Configuration.register_database database
60
+
61
+ @database.setup! options if setup_on_initialization && @database === database
62
+ end
63
+
64
+ def initialize_design
65
+ @design = Design.new @database, self, :id => self.to_s.underscore
66
+ Configuration.register_design @design
67
+ end
68
+
69
+ def generate_class_view
70
+ @design.generate_view Configuration::CLASS_VIEW_NAME
71
+ end
72
+
73
+ def push_design(options)
74
+ setup_on_initialization = options[:setup_on_initialization] || false
75
+ @design.push if setup_on_initialization
76
+ end
77
+
78
+ def find_view(name)
79
+ @design ? @design.views.select{ |view| view.name == name.to_s }.first : nil
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "core_extension", "string"))
2
+ require File.join(File.dirname(__FILE__), "configuration")
3
+ require File.join(File.dirname(__FILE__), "transport")
4
+ require File.join(File.dirname(__FILE__), "server")
5
+ require File.join(File.dirname(__FILE__), "database")
6
+ require File.join(File.dirname(__FILE__), "design")
7
+ require File.join(File.dirname(__FILE__), "base", "setup")
8
+ require File.join(File.dirname(__FILE__), "base", "accessor")
9
+ require File.join(File.dirname(__FILE__), "base", "finder")
10
+ require File.join(File.dirname(__FILE__), "base", "association")
11
+ require 'uri'
12
+
13
+ module CouchModel
14
+
15
+ class Base
16
+ include CouchModel::Base::Setup
17
+ include CouchModel::Base::Accessor
18
+ include CouchModel::Base::Finder
19
+ include CouchModel::Base::Association
20
+
21
+ class Error < StandardError; end
22
+ class NotFoundError < StandardError; end
23
+
24
+ attr_reader :attributes
25
+
26
+ def initialize(attributes = { })
27
+ @attributes = { Configuration::CLASS_KEY => self.class.to_s }
28
+ self.attributes = attributes
29
+ end
30
+
31
+ def attributes=(attributes)
32
+ attributes.each { |key, value| self.send :"#{key}=", value if self.respond_to?(:"#{key}=") }
33
+ end
34
+
35
+ def id
36
+ @attributes["_id"]
37
+ end
38
+ alias :_id :id
39
+
40
+ def id=(value)
41
+ @attributes["_id"] = value
42
+ end
43
+
44
+ def rev
45
+ @attributes["_rev"]
46
+ end
47
+ alias :_rev :rev
48
+
49
+ def ==(other)
50
+ self.id == other.id
51
+ end
52
+
53
+ def new?
54
+ self.rev.nil?
55
+ end
56
+
57
+ def load
58
+ response = Transport.request :get, url, :expected_status_code => 200
59
+
60
+ self.rev = response["_rev"]
61
+ [ "_id", "_rev", Configuration::CLASS_KEY ].each { |key| response.delete key }
62
+ self.attributes = response
63
+ true
64
+ rescue Transport::UnexpectedStatusCodeError => e
65
+ raise NotFoundError if e.status_code == 404
66
+ raise e
67
+ end
68
+
69
+ def save
70
+ new? ? create : update
71
+ end
72
+
73
+ def destroy
74
+ return false if new?
75
+ Transport.request :delete, self.url, :parameters => { "rev" => self.rev }, :expected_status_code => 200
76
+ self.rev = nil
77
+ true
78
+ rescue Transport::UnexpectedStatusCodeError => e
79
+ raise NotFoundError if e.status_code == 404
80
+ raise e
81
+ end
82
+
83
+ def url
84
+ "#{self.database.url}/#{self.id}"
85
+ end
86
+
87
+ def method_missing(method_name, *arguments, &block)
88
+ return @attributes[Configuration::CLASS_KEY] if Configuration::CLASS_KEY == method_name.to_s
89
+ super
90
+ end
91
+
92
+ private
93
+
94
+ def rev=(value)
95
+ @attributes["_rev"] = value
96
+ end
97
+
98
+ def create
99
+ response = Transport.request :post, self.database.url, :json => self.attributes, :expected_status_code => 201
100
+ self.id = response["id"]
101
+ self.rev = response["rev"]
102
+ true
103
+ rescue Transport::UnexpectedStatusCodeError
104
+ false
105
+ end
106
+
107
+ def update
108
+ response = Transport.request :put, self.url, :json => self.attributes, :expected_status_code => 200
109
+ self.rev = response["rev"]
110
+ true
111
+ rescue Transport::UnexpectedStatusCodeError
112
+ false
113
+ end
114
+
115
+ end
116
+
117
+ end