couchbase 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/HISTORY.markdown +50 -0
- data/LICENSE +201 -0
- data/README.markdown +135 -0
- data/Rakefile +27 -0
- data/couchbase.gemspec +28 -0
- data/lib/couchbase.rb +57 -0
- data/lib/couchbase/bucket.rb +138 -0
- data/lib/couchbase/couchdb.rb +107 -0
- data/lib/couchbase/document.rb +71 -0
- data/lib/couchbase/http_status.rb +118 -0
- data/lib/couchbase/latch.rb +71 -0
- data/lib/couchbase/memcached.rb +372 -0
- data/lib/couchbase/node.rb +49 -0
- data/lib/couchbase/rest_client.rb +124 -0
- data/lib/couchbase/version.rb +20 -0
- data/lib/couchbase/view.rb +182 -0
- data/test/setup.rb +28 -0
- data/test/support/buckets-config.json +843 -0
- data/test/support/sample_design_doc.json +9 -0
- data/test/test_bucket.rb +39 -0
- data/test/test_couchbase.rb +9 -0
- data/test/test_couchdb.rb +98 -0
- data/test/test_document.rb +11 -0
- data/test/test_latch.rb +88 -0
- data/test/test_memcached.rb +59 -0
- data/test/test_rest_client.rb +14 -0
- data/test/test_version.rb +7 -0
- data/test/test_view.rb +98 -0
- metadata +164 -0
data/lib/couchbase.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Author:: Couchbase <info@couchbase.com>
|
2
|
+
# Copyright:: 2011 Couchbase, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Couchbase
|
19
|
+
autoload :Bucket, 'couchbase/bucket'
|
20
|
+
autoload :Couchdb, 'couchbase/couchdb'
|
21
|
+
autoload :Document, 'couchbase/document'
|
22
|
+
autoload :HttpStatus, 'couchbase/http_status'
|
23
|
+
autoload :Latch, 'couchbase/latch'
|
24
|
+
autoload :Memcached, 'couchbase/memcached'
|
25
|
+
autoload :Node, 'couchbase/node'
|
26
|
+
autoload :RestClient, 'couchbase/rest_client'
|
27
|
+
autoload :VERSION, 'couchbase/version'
|
28
|
+
autoload :View, 'couchbase/view'
|
29
|
+
|
30
|
+
# This error is raising when library detects that some operation
|
31
|
+
# doesn't implemented by the server. For example views API doesn't
|
32
|
+
# implemented by Membase 1.7.x
|
33
|
+
class NotImplemented < Exception; end
|
34
|
+
|
35
|
+
class ViewError < Exception
|
36
|
+
attr_reader :from, :reason
|
37
|
+
|
38
|
+
def initialize(from, reason)
|
39
|
+
@from = from
|
40
|
+
@reason = reason
|
41
|
+
super("#{from}: #{reason}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
# The method +new+ initializes new Bucket instance with all arguments passed.
|
47
|
+
#
|
48
|
+
# === Examples
|
49
|
+
# Couchbase.new("http://localhost:8091/pools/default") #=> establish connection with couchbase default pool and default bucket
|
50
|
+
# Couchbase.new("http://localhost:8091/pools/default", :bucket_name => 'blog') #=> select custom bucket
|
51
|
+
# Couchbase.new("http://localhost:8091/pools/default", :bucket_name => 'blog', :bucket_password => 'secret') #=> specify password for bucket (and SASL auth for memcached client)
|
52
|
+
def new(*args)
|
53
|
+
Bucket.new(*args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# Author:: Couchbase <info@couchbase.com>
|
2
|
+
# Copyright:: 2011 Couchbase, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'uri'
|
19
|
+
|
20
|
+
module Couchbase
|
21
|
+
|
22
|
+
# This class in charge of all stuff connected to communication with
|
23
|
+
# Couchbase. It includes CouchDB and Memcached APIs. Also it includes
|
24
|
+
# methods for HTTP transport from RestClient.
|
25
|
+
|
26
|
+
class Bucket
|
27
|
+
include RestClient
|
28
|
+
include Couchdb
|
29
|
+
include Memcached
|
30
|
+
|
31
|
+
attr_accessor :pool_uri, :environment, :type, :nodes,
|
32
|
+
:streaming_uri, :name, :uri, :vbuckets
|
33
|
+
|
34
|
+
# Initializes connection using +pool_uri+ and optional
|
35
|
+
# +:bucket_name+ and +:bucket_password+ (for protected buckets). Bucket
|
36
|
+
# name will be used as a username for all authorizations (SASL for
|
37
|
+
# Memcached API and Basic for HTTP). It also accepts +:environment+
|
38
|
+
# parameter wich intended to let library know what mode it should
|
39
|
+
# use when it applicable (for example it skips/preserves design
|
40
|
+
# documents with 'dev_' prefix for CouchDB API). You can specify
|
41
|
+
# any string starting from 'dev' or 'test' to activate development
|
42
|
+
# mode.
|
43
|
+
#
|
44
|
+
# Also starts thread which will simultanuously listen for
|
45
|
+
# configuration update via +streaming_uri+. Server should push
|
46
|
+
# update notification about adding or removing nodes from cluster.
|
47
|
+
#
|
48
|
+
# Raises ArgumentError when it cannot find specified bucket in given
|
49
|
+
# pool.
|
50
|
+
def initialize(pool_uri, options = {})
|
51
|
+
@latch = Latch.new(:in_progress, :ready)
|
52
|
+
@name = options[:bucket_name] || "default"
|
53
|
+
@pool_uri = URI.parse(pool_uri)
|
54
|
+
@environment = if options[:environment].to_s =~ /^(dev|test)/
|
55
|
+
:development
|
56
|
+
else
|
57
|
+
:production
|
58
|
+
end
|
59
|
+
config = http_get("#{@pool_uri}/buckets").detect do |bucket|
|
60
|
+
bucket['name'] == @name
|
61
|
+
end
|
62
|
+
unless config
|
63
|
+
raise ArgumentError,
|
64
|
+
"There no such bucket with name '#{@name}' in pool #{pool_uri}"
|
65
|
+
end
|
66
|
+
@uri = @pool_uri.merge(config['uri'])
|
67
|
+
@streaming_uri = @pool_uri.merge(config['streamingUri'])
|
68
|
+
if password = options[:bucket_password]
|
69
|
+
@credentials = {:username => @name, :password => password}
|
70
|
+
end
|
71
|
+
super
|
72
|
+
|
73
|
+
# Considering all initialization stuff completed and now we can
|
74
|
+
# start config listener
|
75
|
+
listen_for_config_changes
|
76
|
+
|
77
|
+
@latch.wait
|
78
|
+
end
|
79
|
+
|
80
|
+
# Select next node for work with Couchbase. Currently it makes sense
|
81
|
+
# only for couchdb API, because memcached client works using moxi.
|
82
|
+
def next_node
|
83
|
+
nodes.shuffle.first
|
84
|
+
end
|
85
|
+
|
86
|
+
# Perform configuration using configuration cache. It turn all URIs
|
87
|
+
# into full form (with schema, host and port).
|
88
|
+
#
|
89
|
+
# You can override this method in included modules or descendants if
|
90
|
+
# you'd like to reconfigure them when new configuration arrives from
|
91
|
+
# server.
|
92
|
+
def setup(config)
|
93
|
+
@type = config['bucketType']
|
94
|
+
@nodes = config['nodes'].map do |node|
|
95
|
+
Node.new(node['status'],
|
96
|
+
node['hostname'].split(':').first,
|
97
|
+
node['ports'],
|
98
|
+
node['couchApiBase'])
|
99
|
+
end
|
100
|
+
if @type == 'membase'
|
101
|
+
@vbuckets = config['vBucketServerMap']['vBucketMap']
|
102
|
+
end
|
103
|
+
super
|
104
|
+
@latch.toggle
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Run background thread to listen for configuration changes.
|
110
|
+
# Rewrites configuration for each update. Curl::Multi uses select()
|
111
|
+
# call when waiting for data, so is should be efficient use ruby
|
112
|
+
# threads here.
|
113
|
+
def listen_for_config_changes
|
114
|
+
Thread.new do
|
115
|
+
multi = Curl::Multi.new
|
116
|
+
multi.add(mk_curl(@streaming_uri.to_s))
|
117
|
+
multi.perform
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def mk_curl(url)
|
122
|
+
Curl::Easy.new(url) do |curl|
|
123
|
+
curl.useragent = "couchbase-ruby-client/#{Couchbase::VERSION}"
|
124
|
+
if @credentials
|
125
|
+
curl.http_auth_types = :basic
|
126
|
+
curl.username = @credentials[:username]
|
127
|
+
curl.password = @credentials[:password]
|
128
|
+
end
|
129
|
+
curl.verbose = true if Kernel.respond_to?(:debugger)
|
130
|
+
curl.on_body do |data|
|
131
|
+
config = Yajl::Parser.parse(data)
|
132
|
+
setup(config) if config
|
133
|
+
data.bytesize
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# Author:: Couchbase <info@couchbase.com>
|
2
|
+
# Copyright:: 2011 Couchbase, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Couchbase
|
19
|
+
module Couchdb
|
20
|
+
|
21
|
+
# Initializes CouchDB related part of connection.
|
22
|
+
#
|
23
|
+
# @param [ String ] pool_uri Couchbase pool URI.
|
24
|
+
#
|
25
|
+
# @param [ Hash ] options Connection options. This is the hash the client
|
26
|
+
# passed to Couchbase.new to start the session
|
27
|
+
def initialize(pool_uri, options = {})
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
# Fetch design docs stored in current bucket
|
32
|
+
#
|
33
|
+
# @return [ Hash ]
|
34
|
+
def design_docs
|
35
|
+
docs = http_get("#{next_node.couch_api_base}/_all_docs",
|
36
|
+
:params => {:startkey => "_design/", :endkey => "_design0", :include_docs => true})
|
37
|
+
result = {}
|
38
|
+
docs['rows'].each do |doc|
|
39
|
+
doc = Document.wrap(self, doc)
|
40
|
+
key = doc['_id'].sub(/^_design\//, '')
|
41
|
+
next if @environment == :production && key =~ /dev_/
|
42
|
+
result[key] = doc
|
43
|
+
end
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
# Update or create design doc with supplied views
|
48
|
+
#
|
49
|
+
# @param [ Hash, IO, String ] data The source object containing JSON
|
50
|
+
# encoded design document. It must have
|
51
|
+
# <tt>_id</tt> key set, this key should
|
52
|
+
# start with <tt>_design/</tt>.
|
53
|
+
#
|
54
|
+
# @return [ Couchbase::Document ] instance
|
55
|
+
def save_design_doc(data)
|
56
|
+
doc = parse_design_document(data)
|
57
|
+
rv = http_put("#{next_node.couch_api_base}/#{doc['_id']}", {}, doc)
|
58
|
+
doc['_rev'] = rv['rev']
|
59
|
+
doc
|
60
|
+
end
|
61
|
+
|
62
|
+
# Fetch all documents from the bucket.
|
63
|
+
#
|
64
|
+
# @param [ Hash ] params Params for CouchDB <tt>/_all_docs</tt> query
|
65
|
+
#
|
66
|
+
# @return [ Couchbase::View ] View object
|
67
|
+
def all_docs(params = {})
|
68
|
+
View.new(self, "#{next_node.couch_api_base}/_all_docs", params)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Delete design doc with given id and revision.
|
72
|
+
#
|
73
|
+
# @param [ String ] id Design document id. It might have '_design/'
|
74
|
+
# prefix.
|
75
|
+
#
|
76
|
+
# @param [ String ] rev Document revision. It uses latest revision if
|
77
|
+
# <tt>rev</tt> parameter is nil.
|
78
|
+
#
|
79
|
+
def delete_design_doc(id, rev = nil)
|
80
|
+
ddoc = design_docs[id.sub(/^_design\//, '')]
|
81
|
+
return nil unless ddoc
|
82
|
+
http_delete("#{next_node.couch_api_base}/#{ddoc['_id']}",
|
83
|
+
:params => {:rev => rev || ddoc['_rev']})
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def parse_design_document(doc)
|
89
|
+
data = case doc
|
90
|
+
when String
|
91
|
+
Yajl::Parser.parse(doc)
|
92
|
+
when IO
|
93
|
+
Yajl::Parser.parse(doc.read)
|
94
|
+
when Hash
|
95
|
+
doc
|
96
|
+
else
|
97
|
+
raise ArgumentError, "Document should be Hash, String or IO instance"
|
98
|
+
end
|
99
|
+
|
100
|
+
if data['_id'].to_s !~ /^_design\//
|
101
|
+
raise ArgumentError, "'_id' key must be set and start with '_design/'."
|
102
|
+
end
|
103
|
+
data['language'] ||= 'javascript'
|
104
|
+
Document.wrap(self, data)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Author:: Couchbase <info@couchbase.com>
|
2
|
+
# Copyright:: 2011 Couchbase, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Couchbase
|
19
|
+
class Document
|
20
|
+
# Undefine as much methods as we can to free names for views
|
21
|
+
instance_methods.each do |m|
|
22
|
+
undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$|^class$|)/
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :data, :views
|
26
|
+
|
27
|
+
def initialize(connection, data)
|
28
|
+
@data = data
|
29
|
+
@connection = connection
|
30
|
+
@views = []
|
31
|
+
begin
|
32
|
+
if design_doc?
|
33
|
+
data['views'].each do |name, funs|
|
34
|
+
@views << name
|
35
|
+
self.instance_eval <<-EOV, __FILE__, __LINE__ + 1
|
36
|
+
def #{name}(params = {})
|
37
|
+
endpoint = "\#{@connection.next_node.couch_api_base}/\#{@data['_id']}/_view/#{name}"
|
38
|
+
View.new(@connection, endpoint, params)
|
39
|
+
end
|
40
|
+
EOV
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue NoMethodError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.wrap(connection, data)
|
48
|
+
Document.new(connection, data['doc'] || data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def [](key)
|
52
|
+
@data[key]
|
53
|
+
end
|
54
|
+
|
55
|
+
def []=(key, value)
|
56
|
+
@data[key] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def design_doc?
|
60
|
+
!!(@data['_id'] =~ %r(_design/))
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_views?
|
64
|
+
!!(design_doc? && !@views.empty?)
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspect
|
68
|
+
%(#<#{self.class.name}:#{self.object_id} #{@data.inspect}>)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# Author:: Couchbase <info@couchbase.com>
|
2
|
+
# Copyright:: 2011 Couchbase, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Couchbase
|
19
|
+
|
20
|
+
# This module contains definitions for exceptions which wrap HTTP
|
21
|
+
# errors. Also it collect them in useful structure
|
22
|
+
# +HttpStatus::Errors+ which is map with status code as a key and
|
23
|
+
# exception class as a value. See the usage example below.
|
24
|
+
#
|
25
|
+
# === Example
|
26
|
+
#
|
27
|
+
# data = Yajl::Parser.parse(curl.body_str)
|
28
|
+
# if error_class = HttpStatus::Errors[curl.response_code]
|
29
|
+
# raise error_class.new(data['error'], data['reason'])
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
|
33
|
+
module HttpStatus
|
34
|
+
|
35
|
+
# Base class for all HTTP error codes. It povides handy methods for
|
36
|
+
# investigating what happened. For example, it stores +status_code+
|
37
|
+
# and human readable +status_message+ automatically and allows to
|
38
|
+
# provide context specific code and message (+error+ and +reason+
|
39
|
+
# correspondingly)
|
40
|
+
|
41
|
+
class Status < Exception
|
42
|
+
class << self
|
43
|
+
attr_accessor :status_code, :status_message
|
44
|
+
alias_method :to_i, :status_code
|
45
|
+
|
46
|
+
def status_line
|
47
|
+
"#{status_code} #{status_message}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :error, :reason
|
52
|
+
def initialize(error, reason)
|
53
|
+
@error = error
|
54
|
+
@reason = reason
|
55
|
+
super("#{error}: #{reason}")
|
56
|
+
end
|
57
|
+
|
58
|
+
def status_code
|
59
|
+
self.class.status_code
|
60
|
+
end
|
61
|
+
|
62
|
+
def status_message
|
63
|
+
self.class.status_message
|
64
|
+
end
|
65
|
+
|
66
|
+
def status_line
|
67
|
+
self.class.status_line
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_i
|
71
|
+
self.class.to_i
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
StatusMessage = {
|
76
|
+
400 => 'Bad Request',
|
77
|
+
401 => 'Unauthorized',
|
78
|
+
402 => 'Payment Required',
|
79
|
+
403 => 'Forbidden',
|
80
|
+
404 => 'Not Found',
|
81
|
+
405 => 'Method Not Allowed',
|
82
|
+
406 => 'Not Acceptable',
|
83
|
+
407 => 'Proxy Authentication Required',
|
84
|
+
408 => 'Request Timeout',
|
85
|
+
409 => 'Conflict',
|
86
|
+
410 => 'Gone',
|
87
|
+
411 => 'Length Required',
|
88
|
+
412 => 'Precondition Failed',
|
89
|
+
413 => 'Request Entity Too Large',
|
90
|
+
414 => 'Request-URI Too Large',
|
91
|
+
415 => 'Unsupported Media Type',
|
92
|
+
416 => 'Request Range Not Satisfiable',
|
93
|
+
417 => 'Expectation Failed',
|
94
|
+
422 => 'Unprocessable Entity',
|
95
|
+
423 => 'Locked',
|
96
|
+
424 => 'Failed Dependency',
|
97
|
+
500 => 'Internal Server Error',
|
98
|
+
501 => 'Not Implemented',
|
99
|
+
502 => 'Bad Gateway',
|
100
|
+
503 => 'Service Unavailable',
|
101
|
+
504 => 'Gateway Timeout',
|
102
|
+
505 => 'HTTP Version Not Supported',
|
103
|
+
507 => 'Insufficient Storage'
|
104
|
+
}
|
105
|
+
|
106
|
+
# Hash with error code as a key and exceptin class as a value
|
107
|
+
Errors = {}
|
108
|
+
|
109
|
+
StatusMessage.each do |status_code, status_message|
|
110
|
+
klass = Class.new(Status)
|
111
|
+
klass.status_code = status_code
|
112
|
+
klass.status_message = status_message
|
113
|
+
klass_name = status_message.gsub(/[ \-]/,'')
|
114
|
+
const_set(klass_name, klass)
|
115
|
+
HttpStatus::Errors[status_code] = const_get(klass_name)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|