couchmodel 0.1.0.beta2

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 (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