prx_hyperresource 1.0.0

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.
@@ -0,0 +1,194 @@
1
+ require "uri"
2
+
3
+ class HyperResource
4
+ ## HyperResource::Configuration is a class which implements a hostmask-
5
+ ## scoped set of configurations. Key/value pairs are stored under hostmasks
6
+ ## like 'api.example.com:8080', 'api.example.com', '*.example.com', or '*'.
7
+ ## Values are retrieved using a hostname and a key, preferring more specific
8
+ ## hostmasks when more than one matching hostmask and key are present.
9
+ ##
10
+ ## HyperResource users are not expected to use this class directly.
11
+ class Configuration
12
+ ## Creates a new HyperResource::Configuration, with the given initial
13
+ ## internal state if provided.
14
+ def initialize(cfg = {})
15
+ @cfg = cfg
16
+ @cfg["*"] ||= {}
17
+ end
18
+
19
+ ## Returns a deep copy of this object.
20
+ def clone
21
+ self.class.new.send(:initialize_copy, self)
22
+ end
23
+
24
+ ## Merges a given Configuration with this one. Deep-merges config
25
+ ## attributes correctly.
26
+ def merge(new)
27
+ merged_cfg = {}
28
+ new_cfg = new.send(:cfg)
29
+
30
+ (new_cfg.keys | cfg.keys).each do |mask|
31
+ new_cfg_attrs = new_cfg[mask] || {}
32
+ cfg_attrs = cfg[mask] || {}
33
+ merged_cfg[mask] = {}
34
+
35
+ ## Do a hash merge when it makes sense, except when it doesn't.
36
+ (new_cfg_attrs.keys | cfg_attrs.keys).each do |attr|
37
+ merged_cfg[mask][attr] = if !%w[namespace adapter].include?(attr) &&
38
+ (!cfg_attrs[attr] || cfg_attrs[attr].is_a?(Hash)) &&
39
+ (!new_cfg_attrs[attr] || new_cfg_attrs[attr].is_a?(Hash))
40
+ (cfg_attrs[attr] || {}).merge(new_cfg_attrs[attr] || {})
41
+ else
42
+ new_cfg_attrs[attr] || cfg_attrs[attr]
43
+ end
44
+ end
45
+ end
46
+ self.class.new(merged_cfg)
47
+ end
48
+
49
+ ## Applies a given Configuration on top of this one.
50
+ def merge!(new)
51
+ initialize_copy(merge(new))
52
+ end
53
+
54
+ ## Applies a given Hash of configurations on top of this one.
55
+ def config(hash)
56
+ merge!(self.class.new(hash))
57
+ end
58
+
59
+ ## Returns this object as a Hash.
60
+ def as_hash
61
+ clone.send(:cfg)
62
+ end
63
+
64
+ ## Returns the value for a particular hostmask and key, or nil if not
65
+ ## present.
66
+ def get(mask, key)
67
+ cfg[mask] ||= {}
68
+ cfg[mask][key.to_s]
69
+ end
70
+
71
+ ## Sets a key and value pair for the given hostmask.
72
+ def set(mask, key, value)
73
+ cfg[mask] ||= {}
74
+ cfg[mask][key.to_s] = value
75
+ end
76
+
77
+ ## Returns the best matching value for the given URL and key, or nil
78
+ ## otherwise.
79
+ def get_for_url(url, key)
80
+ subconfig_for_url(url)[key.to_s]
81
+ end
82
+
83
+ ## Sets a key and value pair, using the given URL as the basis of the
84
+ ## hostmask. Path, query, and fragment are ignored.
85
+ def set_for_url(url, key, value)
86
+ set(url ? mask_for_url(url) : "*", key, value)
87
+ end
88
+
89
+ ## Returns hostmasks from our config which match the given url, sorted
90
+ ## best match first.
91
+ def matching_masks_for_url(url)
92
+ return ["*"] if cfg.keys.count == 1
93
+ url_parts = parse_url(url.to_s)
94
+ scores = {}
95
+ cfg.keys.each do |mask|
96
+ score = match_mask(parse_url(mask), url_parts)
97
+ scores[mask] = score if score
98
+ end
99
+ scores.keys.sort_by { |k| [-scores[k], -k.length] }
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :cfg
105
+
106
+ ## Performs a two-level-deep copy of old @cfg.
107
+ def initialize_copy(old)
108
+ old.send(:cfg).each do |mask, old_subcfg|
109
+ new_subcfg = {}
110
+ old_subcfg.each do |key, value|
111
+ if value.is_a?(Array) || value.is_a?(Hash)
112
+ value = value.clone
113
+ end
114
+ new_subcfg[key] = value
115
+ end
116
+ @cfg[mask] = new_subcfg
117
+ end
118
+ self
119
+ end
120
+
121
+ ## Returns a merged subconfig consisting of all matching hostmask
122
+ ## subconfigs, giving priority to more specific hostmasks.
123
+ def subconfig_for_url(url)
124
+ matching_masks_for_url(url).inject({}) do |subcfg, mask|
125
+ (cfg[mask] || {}).merge(subcfg)
126
+ end
127
+ end
128
+
129
+ ## Adapted from FuzzyURL.url_to_hash in fuzzyurl 0.2.2
130
+ ## (https://github.com/gamache/fuzzyurl), keeping only protocol, host, and port.
131
+ URL_PATTERN = %r{\A(?:(\*|[a-z][a-z0-9+.-]*)://)?(?:[^@/]*@)?([a-z0-9.*-]*)(?::(\*|\d+))?}i
132
+
133
+ DEFAULT_PORTS = {"http" => "80", "https" => "443"}
134
+
135
+ ## Returns the protocol, host, and port of a URL or hostmask.
136
+ def parse_url(url)
137
+ m = url.match(URL_PATTERN)
138
+ {protocol: m[1]&.downcase, host: m[2].downcase, port: m[3]}
139
+ end
140
+
141
+ ## Returns the hostmask for a URL, without path, query, or fragment.
142
+ def mask_for_url(url)
143
+ parts = parse_url(url.to_s)
144
+ mask = parts[:host]
145
+ mask = "#{parts[:protocol]}://#{mask}" if parts[:protocol]
146
+ mask = "#{mask}:#{parts[:port]}" if parts[:port]
147
+ mask
148
+ end
149
+
150
+ ## Returns nil if the mask does not match the url, or a score which is
151
+ ## higher for more specific matches.
152
+ def match_mask(mask, url)
153
+ host_score = match_hosts(mask[:host], url[:host])
154
+ port_score = match_protocols_and_ports(mask, url)
155
+ host_score + port_score if host_score && port_score
156
+ end
157
+
158
+ ## Returns nil for no match, 0 for a wildcard match, or 1 for an exact
159
+ ## match.
160
+ def match_hosts(mask, host)
161
+ mask_pieces = mask.split(".").reverse
162
+ host_pieces = host.split(".").reverse
163
+ return 1 if !mask.empty? && mask_pieces == host_pieces
164
+ return nil if mask_pieces.count > host_pieces.count
165
+ host_pieces.each_with_index do |piece, i|
166
+ return 0 if mask_pieces[i] == "*"
167
+ return nil if mask_pieces[i] != piece
168
+ end
169
+ 0
170
+ end
171
+
172
+ ## Returns nil for no match, or 0-2 for the number of exact matches.
173
+ ## Protocol defaults to https, and a port matches its protocol's default.
174
+ def match_protocols_and_ports(mask, url)
175
+ wildcards = 0
176
+ protocol = url[:protocol] || "https"
177
+ if mask[:protocol] && mask[:protocol] != "*"
178
+ return nil if mask[:protocol] != protocol
179
+ else
180
+ wildcards += 1
181
+ end
182
+
183
+ if !mask[:port] || mask[:port] == "*"
184
+ wildcards += 1
185
+ elsif !url[:port] && DEFAULT_PORTS[protocol] == mask[:port]
186
+ wildcards += 1
187
+ elsif mask[:port] != url[:port]
188
+ return nil
189
+ end
190
+
191
+ 2 - wildcards
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,41 @@
1
+ class HyperResource
2
+ class Exception < ::StandardError
3
+ ## The internal exception which led to this one, if any.
4
+ attr_accessor :cause
5
+
6
+ # @private
7
+ def initialize(message, attrs = {})
8
+ self.cause = attrs[:cause]
9
+ super(message)
10
+ end
11
+ end
12
+
13
+ class ResponseError < Exception
14
+ ## The +Faraday::Response+ object which led to this exception.
15
+ attr_accessor :response
16
+
17
+ ## The deserialized response body which led to this exception.
18
+ ## May be blank, e.g. in case of deserialization errors.
19
+ attr_accessor :body
20
+
21
+ # @private
22
+ def initialize(message, attrs = {})
23
+ self.response = attrs[:response]
24
+ self.body = attrs[:body]
25
+
26
+ ## Try to help out with the message
27
+ if body
28
+ if (error = body["error"])
29
+ message = "#{message} (#{error})"
30
+ end
31
+ elsif response
32
+ message = "#{message} (\"#{response.inspect}\")"
33
+ end
34
+
35
+ super
36
+ end
37
+ end
38
+
39
+ class ClientError < ResponseError; end
40
+ class ServerError < ResponseError; end
41
+ end
@@ -0,0 +1,116 @@
1
+ require "uri_template"
2
+ require "hyper_resource/modules/http"
3
+
4
+ class HyperResource
5
+ ## HyperResource::Link is an object to represent a hyperlink and its
6
+ ## URL or body parameters, and to encapsulate HTTP calls involving this
7
+ ## link. Links are typically created by HyperResource, not by end users.
8
+ ##
9
+ ## HTTP method calls return the response as a HyperResource (or subclass)
10
+ ## object. Calling an unrecognized method on a link will automatically
11
+ ## load the resource pointed to by this link, and repeat the method call
12
+ ## on the resource object.
13
+ ##
14
+ ## A HyperResource::Link requires the resource it is based upon to remain
15
+ ## in scope. In practice this is rarely a problem, as links are almost
16
+ ## always accessed through the resource object.
17
+
18
+ class Link
19
+ include HyperResource::Modules::HTTP
20
+
21
+ ## The literal href of this link; may be templated.
22
+ attr_accessor :base_href
23
+
24
+ ## An optional name describing this link.
25
+ attr_accessor :name
26
+
27
+ ## `true` if this link's href is a URI Template, `false` otherwise.
28
+ attr_accessor :templated
29
+
30
+ ## A hash of URL or request body parameters.
31
+ attr_accessor :params
32
+
33
+ ## Default HTTP method for implicit loading.
34
+ attr_accessor :default_method
35
+
36
+ ## The resource from which this link originates.
37
+ attr_accessor :resource
38
+
39
+ ## Returns a link based on the given resource and link specification
40
+ ## hash. `link_spec` keys are: `href` (string, required), `templated`
41
+ ## (boolean), `params` (hash), and `default_method` (string, default
42
+ ## `"get"`).
43
+ def initialize(resource, link_spec = {})
44
+ unless link_spec.is_a?(Hash)
45
+ raise ArgumentError, "link_spec must be a Hash (got #{link_spec.inspect})"
46
+ end
47
+ link_spec = link_spec.map { |(k, v)| [k.to_s, v] }.to_h ## stringify keys
48
+
49
+ self.resource = resource
50
+ self.base_href = link_spec["href"]
51
+ self.name = link_spec["name"]
52
+ self.templated = !!link_spec["templated"]
53
+ self.params = link_spec["params"] || {}
54
+ self.default_method = link_spec["method"] || "get"
55
+ @headers = link_spec["headers"] || {}
56
+ end
57
+
58
+ ## Returns this link's href, applying any URI template params.
59
+ def href
60
+ if templated
61
+ filtered_params = resource.outgoing_uri_filter(params)
62
+ URITemplate.new(base_href).expand(filtered_params)
63
+ else
64
+ base_href
65
+ end
66
+ end
67
+
68
+ ## Returns this link's fully resolved URL, or nil if `resource.root`
69
+ ## or `href` are malformed.
70
+ def url
71
+ URI.join(resource.root, href.to_s).to_s
72
+ rescue
73
+ nil
74
+ end
75
+
76
+ ## Returns a new scope with the given params; that is, returns a copy of
77
+ ## itself with the given params applied.
78
+ def where(params)
79
+ params = params.map { |(k, v)| [k.to_s, v] }.to_h
80
+ self.class.new(resource,
81
+ "href" => base_href,
82
+ "name" => name,
83
+ "templated" => templated,
84
+ "params" => self.params.merge(params),
85
+ "method" => default_method,
86
+ "headers" => @headers)
87
+ end
88
+
89
+ ## When called with a hash, returns a new scope with the given headers;
90
+ ## that is, returns a copy of itself with the given headers applied.
91
+ ## These headers will be merged with `resource.headers` at request time.
92
+ ##
93
+ ## When called with no arguments, returns the headers for this link.
94
+ def headers(*args)
95
+ if args.count == 0
96
+ @headers
97
+ else
98
+ self.class.new(resource,
99
+ "href" => base_href,
100
+ "name" => name,
101
+ "templated" => templated,
102
+ "params" => params,
103
+ "method" => default_method,
104
+ "headers" => @headers.merge(args[0]))
105
+
106
+ end
107
+ end
108
+
109
+ ## Unrecognized methods invoke an implicit load of the resource pointed
110
+ ## to by this link. The method call is then repeated on the returned
111
+ ## resource.
112
+ def method_missing(method, *args) # standard:disable Style/MissingRespondToMissing
113
+ send(default_method || :get).send(method, *args)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,66 @@
1
+ class HyperResource
2
+ ## HyperResource::Links is a modified Hash that permits lookup
3
+ ## of a link by its link relation (rel), or an abbreviation thereof.
4
+ ## It also provides read access through `method_missing`.
5
+ ## It is typically created by HyperResource, not by end users.
6
+ ##
7
+ ## For example, a link with rel `someapi:widgets` is accessible
8
+ ## by any of `self.widgets`, `self['widgets']`, `self[:widgets]`, and
9
+ ## `self['someapi:widgets'].
10
+ class Links < Hash
11
+ # @private
12
+ def initialize(resource = nil)
13
+ ## We used to store the resource, but we didn't need to. Now we don't.
14
+ end
15
+
16
+ ## Stores a link for future retrieval by its link rel or abbreviations
17
+ ## thereof.
18
+ def []=(rel, link)
19
+ rel = rel.to_s
20
+
21
+ ## Every link must appear under its literal name.
22
+ names = [rel]
23
+
24
+ ## Extract 'foo' from e.g. 'http://example.com/foo',
25
+ ## 'http://example.com/url#foo', 'somecurie:foo'.
26
+ if (m = rel.match(%r{[:/#](.+)}))
27
+ names << m[1]
28
+ end
29
+
30
+ ## Underscore all non-word characters.
31
+ underscored_names = names.map { |n| n.gsub(/[^a-zA-Z_]/, "_") }
32
+ names = (names + underscored_names).uniq
33
+
34
+ ## Register this link under every name we've come up with.
35
+ names.each do |name|
36
+ super(name, link)
37
+ end
38
+ end
39
+
40
+ ## Retrieves a link by its rel.
41
+ def [](rel)
42
+ super(rel.to_s)
43
+ end
44
+
45
+ ## Provides links.somelink(:a => b) to links.somelink.where(:a => b)
46
+ ## expansion.
47
+ # @private
48
+ def method_missing(method, *args) # standard:disable Style/MissingRespondToMissing
49
+ unless self[method]
50
+ raise NoMethodError, "undefined method `#{method}' for #{inspect}"
51
+ end
52
+
53
+ if args.count > 0
54
+ self[method].where(*args)
55
+ else
56
+ self[method]
57
+ end
58
+ end
59
+
60
+ # @private
61
+ def respond_to?(method, *args)
62
+ return true if has_key?(method.to_s)
63
+ super
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,273 @@
1
+ class HyperResource
2
+ module Modules
3
+ module ConfigAttributes
4
+ ATTRS = [:auth, :headers, :namespace, :adapter, :faraday_options]
5
+
6
+ def self.included(klass)
7
+ klass.extend(ClassMethods)
8
+ end
9
+
10
+ # @private
11
+ def hr_config
12
+ @hr_config ||= self.class::Configuration.new
13
+ end
14
+
15
+ # @private
16
+ def hr_config=(cfg)
17
+ @hr_config = cfg
18
+ end
19
+
20
+ ## When called with no arguments, returns this resource's Configuration.
21
+ ## When called with a hash, applies the given configuration parameters
22
+ ## to this resource's Configuration. `hash` must be in the form:
23
+ ## {'hostmask' => {'attr1' => {...}, 'attr2' => {...}, ...}}
24
+ ## Valid attributes are `auth`, `headers`, `namespace`, `adapter`,
25
+ ## `default_attributes`, and `faraday_options`.
26
+ def config(hash = nil)
27
+ return hr_config unless hash
28
+ hr_config.config(hash)
29
+ end
30
+
31
+ ## Returns the auth config hash for this resource.
32
+ def auth
33
+ cfg_get(:auth)
34
+ end
35
+
36
+ ## Returns the auth config hash for the given url.
37
+ def auth_for_url(url)
38
+ hr_config.get_for_url(url, :auth)
39
+ end
40
+
41
+ ## Sets the auth config hash for this resource.
42
+ ## Currently only the format `{:basic => ['username', 'password']}`
43
+ ## is supported.
44
+ def auth=(v)
45
+ cfg_set(:auth, v)
46
+ end
47
+
48
+ ## Returns the headers hash for this resource.
49
+ ## This is done by merging all applicable header configs.
50
+ def headers
51
+ matching_masks = hr_config.matching_masks_for_url(url)
52
+ matching_masks.inject({}) do |hash, mask|
53
+ (hr_config.get(mask, "headers") || {}).merge(hash)
54
+ end
55
+ end
56
+
57
+ ## Returns the headers hash for the given url.
58
+ def headers_for_url(url)
59
+ hr_config.get_for_url(url, :headers)
60
+ end
61
+
62
+ ## Sets the headers hash for this resource.
63
+ def headers=(v)
64
+ cfg_set(:headers, v)
65
+ end
66
+
67
+ ## Returns the namespace string/class for this resource.
68
+ def namespace
69
+ cfg_get(:namespace)
70
+ end
71
+
72
+ ## Returns the namespace string/class for the given url.
73
+ def namespace_for_url(url)
74
+ hr_config.get_for_url(url, :namespace)
75
+ end
76
+
77
+ ## Sets the namespace string/class for this resource.
78
+ def namespace=(v)
79
+ cfg_set(:namespace, v)
80
+ end
81
+
82
+ ## Returns the adapter class for this resource.
83
+ def adapter
84
+ cfg_get(:adapter) ||
85
+ HyperResource::Adapter::HAL_JSON
86
+ end
87
+
88
+ ## Returns the adapter class for the given url.
89
+ def adapter_for_url(url)
90
+ hr_config.get_for_url(url, :adapter) ||
91
+ HyperResource::Adapter::HAL_JSON
92
+ end
93
+
94
+ ## Sets the adapter class for this resource.
95
+ def adapter=(v)
96
+ cfg_set(:adapter, v)
97
+ end
98
+
99
+ ## Returns the hash of default attributes for this resource.
100
+ def default_attributes
101
+ cfg_get(:default_attributes)
102
+ end
103
+
104
+ ## Returns the hash of default attributes for the given url.
105
+ def default_attributes_for_url(url)
106
+ hr_config.get_for_url(url, :default_attributes)
107
+ end
108
+
109
+ ## Sets the hash of default attributes for this resource.
110
+ ## These attributes will be passed along with every HTTP request.
111
+ def default_attributes=(v)
112
+ cfg_set(:default_attributes, v)
113
+ end
114
+
115
+ ## Returns the Faraday connection options hash for this resource.
116
+ def faraday_options
117
+ cfg_get(:faraday_options)
118
+ end
119
+
120
+ ## Returns the Faraday connection options hash for this resource.
121
+ def faraday_options_for_url(url)
122
+ hr_config.get_for_url(url, :faraday_options)
123
+ end
124
+
125
+ ## Sets the Faraday connection options hash for this resource.
126
+ ## These options will be used for all HTTP requests.
127
+ def faraday_options=(v)
128
+ cfg_set(:faraday_options, v)
129
+ end
130
+
131
+ private
132
+
133
+ def cfg_get(key)
134
+ hr_config.get_for_url(url, key) ||
135
+ self.class.hr_config.get_for_url(url, key)
136
+ end
137
+
138
+ ## Sets a config key-value pair for this resource.
139
+ def cfg_set(key, value)
140
+ hr_config.set_for_url(url.to_s, key, value)
141
+ end
142
+
143
+ public
144
+
145
+ module ClassMethods
146
+ def hr_config
147
+ @hr_config ||= self::Configuration.new
148
+ end
149
+
150
+ ## When called with no arguments, returns this class's Configuration.
151
+ ## When called with a hash, applies the given configuration parameters
152
+ ## to this resource's Configuration. `hash` must be in the form:
153
+ ## {'hostmask' => {'key1' => 'value1', 'key2' => 'value2', ...}}
154
+ ## Valid keys are `auth`, `headers`, `namespace`, `adapter`, and
155
+ ## `faraday_options`.
156
+ def config(hash = nil)
157
+ return hr_config unless hash
158
+ hr_config.config(hash)
159
+ end
160
+
161
+ ## Returns the auth config hash for this resource class.
162
+ def auth
163
+ cfg_get(:auth)
164
+ end
165
+
166
+ ## Returns the auth config hash for the given url.
167
+ def auth_for_url(url)
168
+ hr_config.get_for_url(url, :auth)
169
+ end
170
+
171
+ ## Sets the auth config hash for this resource class.
172
+ ## Currently only the format `{:basic => ['username', 'password']}`
173
+ ## is supported.
174
+ def auth=(v)
175
+ cfg_set(:auth, v)
176
+ end
177
+
178
+ ## Returns the headers hash for this resource class.
179
+ def headers
180
+ cfg_get(:headers)
181
+ end
182
+
183
+ ## Returns the headers hash for the given url.
184
+ def headers_for_url(url)
185
+ hr_config.get_for_url(url, :headers)
186
+ end
187
+
188
+ ## Sets the headers hash for this resource class.
189
+ def headers=(v)
190
+ cfg_set(:headers, v)
191
+ end
192
+
193
+ ## Returns the namespace string/class for this resource class.
194
+ def namespace
195
+ cfg_get(:namespace)
196
+ end
197
+
198
+ ## Returns the namespace string/class for the given url.
199
+ def namespace_for_url(url)
200
+ hr_config.get_for_url(url, :namespace)
201
+ end
202
+
203
+ ## Sets the namespace string/class for this resource class.
204
+ def namespace=(v)
205
+ cfg_set(:namespace, v)
206
+ end
207
+
208
+ ## Returns the adapter class for this resource class.
209
+ def adapter
210
+ cfg_get(:adapter) || HyperResource::Adapter::HAL_JSON
211
+ end
212
+
213
+ ## Returns the adapter class for the given url.
214
+ def adapter_for_url(url)
215
+ hr_config.get_for_url(url, :adapter) ||
216
+ HyperResource::Adapter::HAL_JSON
217
+ end
218
+
219
+ ## Sets the adapter class for this resource class.
220
+ def adapter=(v)
221
+ cfg_set(:adapter, v)
222
+ end
223
+
224
+ ## Returns the hash of default attributes for this resource class.
225
+ def default_attributes
226
+ cfg_get(:default_attributes)
227
+ end
228
+
229
+ ## Returns the hash of default attributes for the given url.
230
+ def default_attributes_for_url(url)
231
+ hr_config.get_for_url(url, :default_attributes)
232
+ end
233
+
234
+ ## Sets the hash of default attributes for this resource class.
235
+ ## These attributes will be passed along with every HTTP request.
236
+ def default_attributes=(v)
237
+ cfg_set(:default_attributes, v)
238
+ end
239
+
240
+ ## Returns the Faraday connection options hash for this resource class.
241
+ def faraday_options
242
+ cfg_get(:faraday_options)
243
+ end
244
+
245
+ ## Returns the Faraday connection options hash for this resource class.
246
+ def faraday_options_for_url(url)
247
+ hr_config.get_for_url(url, :faraday_options)
248
+ end
249
+
250
+ ## Sets the Faraday connection options hash for this resource class.
251
+ ## These options will be used for all HTTP requests.
252
+ def faraday_options=(v)
253
+ cfg_set(:faraday_options, v)
254
+ end
255
+
256
+ private
257
+
258
+ def cfg_get(key)
259
+ value = hr_config.get_for_url(root, key)
260
+ if !value.nil?
261
+ value
262
+ elsif superclass.respond_to?(:hr_config)
263
+ superclass.hr_config.get_for_url(root, key)
264
+ end
265
+ end
266
+
267
+ def cfg_set(key, value)
268
+ hr_config.set_for_url(root, key, value)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end