confstruct 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/README.md +164 -0
- data/Rakefile +3 -0
- data/confstruct.gemspec +26 -0
- data/lib/confstruct.rb +3 -0
- data/lib/confstruct/configuration.rb +53 -0
- data/lib/confstruct/hash_with_struct_access.rb +214 -0
- data/lib/confstruct/utils.rb +13 -0
- data/lib/tasks/rdoc.rake +32 -0
- data/lib/tasks/rspec.rake +17 -0
- data/spec/confstruct/configuration_spec.rb +123 -0
- data/spec/confstruct/hash_with_struct_access_spec.rb +226 -0
- data/spec/confstruct/utils_spec.rb +32 -0
- data/spec/spec_helper.rb +13 -0
- metadata +169 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# confstruct
|
2
|
+
|
3
|
+
<b>Confstruct</b> is yet another configuration gem. Definable and configurable by
|
4
|
+
hash, struct, or block, confstruct aims to provide the flexibility to do things your
|
5
|
+
way, while keeping things simple and intuitive.
|
6
|
+
|
7
|
+
[![Build Status](https://secure.travis-ci.org/mbklein/confstruct.png)](http://travis-ci.org/mbklein/confstruct])
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
First, either create an empty `ConfStruct::Configuration` object:
|
12
|
+
|
13
|
+
config = Confstruct::Configuration.new
|
14
|
+
|
15
|
+
Or with some default values:
|
16
|
+
|
17
|
+
config = Confstruct::Configuration.new({
|
18
|
+
:project => 'confstruct',
|
19
|
+
:github => {
|
20
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
21
|
+
:branch => 'master'
|
22
|
+
}
|
23
|
+
})
|
24
|
+
|
25
|
+
The above can also be done in block form:
|
26
|
+
|
27
|
+
config = Confstruct::Configuration.new do
|
28
|
+
project 'confstruct'
|
29
|
+
github do
|
30
|
+
url 'http://www.github.com/mbklein/confstruct'
|
31
|
+
branch 'master'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
There are many ways to access and configure the resulting `config` object...
|
36
|
+
|
37
|
+
As a struct...
|
38
|
+
|
39
|
+
config.project = 'other-project'
|
40
|
+
config.github.url = 'http://www.github.com/somefork/other-project'
|
41
|
+
config.github.branch = 'pre-1.0'
|
42
|
+
|
43
|
+
As a block...
|
44
|
+
|
45
|
+
config.configure do
|
46
|
+
project 'other-project'
|
47
|
+
github.url 'http://www.github.com/somefork/other-project'
|
48
|
+
github.branch 'pre-1.0'
|
49
|
+
end
|
50
|
+
|
51
|
+
As a hash...
|
52
|
+
|
53
|
+
config[:github][:url] = 'http://www.github.com/somefork/other-project'
|
54
|
+
|
55
|
+
Or even as a crazy struct/hash hybrid...
|
56
|
+
|
57
|
+
config.github[:url] = 'http://www.github.com/somefork/other-project'
|
58
|
+
config[:github].branch = 'pre-1.0'
|
59
|
+
|
60
|
+
Each sub-hash/struct is a configuration object in its own right, and can be
|
61
|
+
treated as such. (Note the ability to leave the `configure` method
|
62
|
+
off the inner call.)
|
63
|
+
|
64
|
+
config.configure do
|
65
|
+
project 'other-project'
|
66
|
+
github do
|
67
|
+
url 'http://www.github.com/somefork/other-project'
|
68
|
+
branch 'pre-1.0'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
You can even
|
73
|
+
|
74
|
+
config.project = 'other-project'
|
75
|
+
config.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
|
76
|
+
|
77
|
+
The configure method will even a deep merge for you if you pass it a hash or hash-like object
|
78
|
+
(anything that responds to `each_pair`)
|
79
|
+
|
80
|
+
config.configure({:project => 'other-project', :github => {:url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0'}})
|
81
|
+
|
82
|
+
Because it's "hashes all the way down," you can store your defaults and/or configurations
|
83
|
+
in YAML files, or in Ruby scripts if you need to evaluate expressions at config-time.
|
84
|
+
|
85
|
+
However you do it, the resulting configuration object can be accessed either as a
|
86
|
+
hash or a struct:
|
87
|
+
|
88
|
+
config.project
|
89
|
+
=> "other-project"
|
90
|
+
config[:project]
|
91
|
+
=> "other-project"
|
92
|
+
config.github
|
93
|
+
=> {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
|
94
|
+
config.github.url
|
95
|
+
=> "http://www.github.com/somefork/other-project"
|
96
|
+
config.github[:url]
|
97
|
+
=> "http://www.github.com/somefork/other-project"
|
98
|
+
config[:github]
|
99
|
+
=> {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
|
100
|
+
|
101
|
+
### Advanced Tips & Tricks
|
102
|
+
|
103
|
+
Any configuration value of class `Confstruct::Deferred` will be evaluated on access, allowing you to
|
104
|
+
define read-only, dynamic configuration attributes
|
105
|
+
|
106
|
+
config[:github][:client] = Confstruct::Deferred.new { |c| RestClient::Resource.new(c.url) }
|
107
|
+
=> #<Proc:0x00000001035eb240>
|
108
|
+
config.github.client
|
109
|
+
=> #<RestClient::Resource:0x1035e3b30 @options={}, @url="http://www.github.com/mbklein/confstruct", @block=nil>
|
110
|
+
config.github.url = 'http://www.github.com/somefork/other-project'
|
111
|
+
=> "http://www.github.com/somefork/other-project"
|
112
|
+
config.github.client
|
113
|
+
=> #<RestClient::Resource:0x1035d5bc0 @options={}, @url="http://www.github.com/somefork/other-project", @block=nil>
|
114
|
+
|
115
|
+
As a convenience, `Confstruct.deferred(&block)` and `Confstruct::HashWithStructAccess#deferred!(&block)`
|
116
|
+
act like `lambda`, making the following two assignments equivalent to the above:
|
117
|
+
|
118
|
+
config.github.client = Confstruct.deferred { |c| RestClient::Resource.new(c.url) }
|
119
|
+
config.github do
|
120
|
+
client deferred! { |c| RestClient::Resource.new(c.url) }
|
121
|
+
end
|
122
|
+
|
123
|
+
`push!` and `pop!` methods allow you to temporarily override some or all of your configuration values. This can be
|
124
|
+
useful in spec tests where you need to change values but don't want to worry about messing up tests that depend
|
125
|
+
on the same global configuration object.
|
126
|
+
|
127
|
+
config.github.url
|
128
|
+
=> "http://www.github.com/mbklein/confstruct"
|
129
|
+
config.push! { github.url 'http://www.github.com/somefork/other-project' }
|
130
|
+
=> {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/somefork/other-project"}}
|
131
|
+
config.github.url
|
132
|
+
=> "http://www.github.com/somefork/other-project"
|
133
|
+
config.pop!
|
134
|
+
=> {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/mbklein/confstruct"}}
|
135
|
+
config.github.url
|
136
|
+
=> "http://www.github.com/mbklein/confstruct"
|
137
|
+
|
138
|
+
### Notes
|
139
|
+
|
140
|
+
* Confstruct will attempt to use ordered hashes internally when available.
|
141
|
+
* In Ruby 1.9 and above, this is automatic.
|
142
|
+
* In Rubies earlier than 1.9, Confstruct will try to require and use ActiveSupport::OrderedHash,
|
143
|
+
falling back to a regular Hash if necessary. The class/instance method `ordered?` can be used
|
144
|
+
to determine if the hash is ordered or not.
|
145
|
+
* In order to support struct access, all hash keys are converted to symbols, and are accessible
|
146
|
+
both as strings and symbols (like a `HashWithIndifferentAccess`). In other words, config['foo']
|
147
|
+
and config[:foo] refer to the same value.
|
148
|
+
|
149
|
+
## Release History
|
150
|
+
|
151
|
+
## Contributing to confstruct
|
152
|
+
|
153
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
154
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
155
|
+
* Fork the project
|
156
|
+
* Start a feature/bugfix branch
|
157
|
+
* Commit and push until you are happy with your contribution
|
158
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
159
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
160
|
+
|
161
|
+
## Copyright
|
162
|
+
|
163
|
+
Copyright (c) 2011 Michael B. Klein. See LICENSE.txt for further details.
|
164
|
+
|
data/Rakefile
ADDED
data/confstruct.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "confstruct"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "confstruct"
|
7
|
+
s.version = Confstruct::VERSION
|
8
|
+
s.authors = ["Michael Klein"]
|
9
|
+
s.email = ["mbklein@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A simple, hash/struct-based configuration object}
|
12
|
+
s.description = %q{A simple, hash/struct-based configuration object}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "rake", ">=0.8.7"
|
20
|
+
s.add_development_dependency "rcov"
|
21
|
+
s.add_development_dependency "rdiscount"
|
22
|
+
s.add_development_dependency "rdoc"
|
23
|
+
s.add_development_dependency "rspec"
|
24
|
+
s.add_development_dependency "yard"
|
25
|
+
|
26
|
+
end
|
data/lib/confstruct.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "confstruct/hash_with_struct_access"
|
2
|
+
|
3
|
+
module Confstruct
|
4
|
+
|
5
|
+
class Configuration < HashWithStructAccess
|
6
|
+
|
7
|
+
def initialize hash=@@hash_class.new, &block
|
8
|
+
super({})
|
9
|
+
@default_values = hash.is_a?(HashWithStructAccess) ? hash : HashWithStructAccess.new(hash)
|
10
|
+
eval_or_yield @default_values, &block
|
11
|
+
reset_defaults!
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_config! obj
|
15
|
+
end
|
16
|
+
|
17
|
+
def configure *args, &block
|
18
|
+
if args[0].respond_to?(:each_pair)
|
19
|
+
self.deep_merge!(args[0])
|
20
|
+
end
|
21
|
+
eval_or_yield self, &block
|
22
|
+
after_config! self
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def push! *args, &block
|
27
|
+
_stash.push(self.deep_copy)
|
28
|
+
configure *args, &block if args.length > 0 or block_given?
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def pop!
|
33
|
+
if _stash.empty?
|
34
|
+
raise IndexError, "Stash is empty"
|
35
|
+
else
|
36
|
+
obj = _stash.pop
|
37
|
+
self.clear
|
38
|
+
self.merge! obj
|
39
|
+
after_config! self
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset_defaults!
|
45
|
+
self.replace(default_values.deep_copy)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
def _stash
|
50
|
+
@stash ||= []
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'confstruct/utils'
|
3
|
+
|
4
|
+
module Confstruct
|
5
|
+
class Deferred < Proc; end
|
6
|
+
def self.deferred █ Deferred.new(&block); end
|
7
|
+
|
8
|
+
if ::RUBY_VERSION < '1.9'
|
9
|
+
begin
|
10
|
+
require 'active_support/ordered_hash'
|
11
|
+
class HashWithStructAccess < DelegateClass(ActiveSupport::OrderedHash); @@ordered = true; @@hash_class = ActiveSupport::OrderedHash; end
|
12
|
+
rescue LoadError, NameError
|
13
|
+
class HashWithStructAccess < DelegateClass(Hash); @@ordered = false; @@hash_class = Hash; end
|
14
|
+
end
|
15
|
+
else
|
16
|
+
class HashWithStructAccess < DelegateClass(Hash); @@ordered = true; @@hash_class = Hash; end
|
17
|
+
end
|
18
|
+
|
19
|
+
class HashWithStructAccess
|
20
|
+
attr_accessor :default_values
|
21
|
+
@default_values = {}
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def from_hash hash
|
25
|
+
symbolized_hash = symbolize_hash hash
|
26
|
+
self.new(symbolized_hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ordered?
|
30
|
+
@@ordered
|
31
|
+
end
|
32
|
+
|
33
|
+
def structurize hash
|
34
|
+
result = hash
|
35
|
+
if result.is_a?(Hash) and not result.is_a?(HashWithStructAccess)
|
36
|
+
result = HashWithStructAccess.new(result)
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def symbolize_hash hash
|
42
|
+
hash.inject(@@hash_class.new) do |h,(k,v)|
|
43
|
+
h[symbolize k] = v.is_a?(Hash) ? symbolize_hash(v) : v
|
44
|
+
h
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def symbolize key
|
49
|
+
(key.to_s.gsub(/\s+/,'_').to_sym rescue key.to_sym) || key
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize hash = @@hash_class.new
|
54
|
+
super(hash)
|
55
|
+
end
|
56
|
+
|
57
|
+
def [] key
|
58
|
+
result = structurize! super(symbolize!(key))
|
59
|
+
if result.is_a?(Deferred)
|
60
|
+
result = eval_or_yield self, &result
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def []= key,value
|
66
|
+
k = symbolize!(key)
|
67
|
+
v = structurize! value
|
68
|
+
if v.is_a?(Hash) and self[k].is_a?(Hash)
|
69
|
+
self[k].replace(v)
|
70
|
+
else
|
71
|
+
super(k, v)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def deep_copy
|
76
|
+
result = self.class.new(@@hash_class.new)
|
77
|
+
self.each_pair do |k,v|
|
78
|
+
if v.respond_to?(:deep_copy)
|
79
|
+
result[k] = v.deep_copy
|
80
|
+
else
|
81
|
+
result[k] = Marshal.load(Marshal.dump(v)) rescue v.dup
|
82
|
+
end
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
alias_method :inheritable_copy, :deep_copy
|
87
|
+
|
88
|
+
def deep_merge hash
|
89
|
+
do_deep_merge! hash, self.deep_copy
|
90
|
+
end
|
91
|
+
|
92
|
+
def deep_merge! hash
|
93
|
+
do_deep_merge! hash, self
|
94
|
+
end
|
95
|
+
|
96
|
+
def deferred! &block
|
97
|
+
Deferred.new(&block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def has? key_path
|
101
|
+
val = self
|
102
|
+
keys = key_path.split(/\./)
|
103
|
+
keys.each do |key|
|
104
|
+
return false if val.nil?
|
105
|
+
if val.respond_to?(:has_key?) and val.has_key?(key.to_sym)
|
106
|
+
val = val[key.to_sym]
|
107
|
+
else
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
return true
|
112
|
+
end
|
113
|
+
|
114
|
+
def inspect
|
115
|
+
r = self.keys.collect { |k| "#{k.inspect}=>#{self[k].inspect}" }
|
116
|
+
"{#{r.compact.join(', ')}}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def is_a? klazz
|
120
|
+
klazz == @@hash_class or super
|
121
|
+
end
|
122
|
+
|
123
|
+
def lookup! key_path
|
124
|
+
val = self
|
125
|
+
keys = key_path.split(/\./)
|
126
|
+
keys.each do |key|
|
127
|
+
return nil if val.nil?
|
128
|
+
if val.respond_to?(:has_key?) and val.has_key?(key.to_sym)
|
129
|
+
val = val[key.to_sym]
|
130
|
+
else
|
131
|
+
return nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
return val
|
135
|
+
end
|
136
|
+
|
137
|
+
def method_missing sym, *args, &block
|
138
|
+
name = sym.to_s.chomp('=').to_sym
|
139
|
+
result = nil
|
140
|
+
|
141
|
+
if name.to_s =~ /^add_(.+)!$/
|
142
|
+
name = $1.to_sym
|
143
|
+
self[name] = [] unless self.has_key?(name)
|
144
|
+
unless self[name].is_a?(Array)
|
145
|
+
raise TypeError, "Cannot #add! to a #{self[name].class}"
|
146
|
+
end
|
147
|
+
if args.length > 0
|
148
|
+
local_args = args.collect { |a| structurize! a }
|
149
|
+
result = self[name].push *local_args
|
150
|
+
elsif block_given?
|
151
|
+
result = HashWithStructAccess.new(@@hash_class.new)
|
152
|
+
self[name].push result
|
153
|
+
end
|
154
|
+
elsif args.length == 1
|
155
|
+
result = self[name] = args[0]
|
156
|
+
elsif args.length > 1
|
157
|
+
super(sym,*args,&block)
|
158
|
+
else
|
159
|
+
result = self[name]
|
160
|
+
if result.nil? and block_given?
|
161
|
+
result = self[name] = HashWithStructAccess.new(@@hash_class.new)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
if block_given?
|
165
|
+
eval_or_yield result, &block
|
166
|
+
end
|
167
|
+
result
|
168
|
+
end
|
169
|
+
|
170
|
+
def methods
|
171
|
+
key_methods = keys.collect do |k|
|
172
|
+
self[k].is_a?(Deferred) ? k.to_s : [k.to_s, "#{k}="]
|
173
|
+
end
|
174
|
+
super + key_methods.compact.flatten
|
175
|
+
end
|
176
|
+
|
177
|
+
def ordered?
|
178
|
+
self.class.ordered?
|
179
|
+
end
|
180
|
+
|
181
|
+
def respond_to? arg
|
182
|
+
super(arg) || keys.include?(symbolize!(arg.to_s.sub(/=$/,'')))
|
183
|
+
end
|
184
|
+
|
185
|
+
def structurize! hash
|
186
|
+
self.class.structurize(hash)
|
187
|
+
end
|
188
|
+
|
189
|
+
def symbolize! key
|
190
|
+
self.class.symbolize(key)
|
191
|
+
end
|
192
|
+
|
193
|
+
def values
|
194
|
+
keys.collect { |k| self[k] }
|
195
|
+
end
|
196
|
+
|
197
|
+
protected
|
198
|
+
def do_deep_merge! source, target
|
199
|
+
source.each_pair do |k,v|
|
200
|
+
if target.has_key?(k)
|
201
|
+
if v.respond_to?(:each_pair) and target[k].respond_to?(:merge)
|
202
|
+
do_deep_merge! v, target[k]
|
203
|
+
elsif v != target[k]
|
204
|
+
target[k] = v
|
205
|
+
end
|
206
|
+
else
|
207
|
+
target[k] = v
|
208
|
+
end
|
209
|
+
end
|
210
|
+
target
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|
data/lib/tasks/rdoc.rake
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
desc "Generate RDoc"
|
2
|
+
task :doc => ['doc:generate']
|
3
|
+
|
4
|
+
namespace :doc do
|
5
|
+
project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
6
|
+
doc_destination = File.join(project_root, 'rdoc')
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'yard'
|
10
|
+
require 'yard/rake/yardoc_task'
|
11
|
+
|
12
|
+
YARD::Rake::YardocTask.new(:generate) do |yt|
|
13
|
+
yt.files = Dir.glob(File.join(project_root, 'lib', '*.rb')) +
|
14
|
+
Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
|
15
|
+
[ File.join(project_root, 'README.rdoc') ] +
|
16
|
+
[ File.join(project_root, 'LICENSE') ]
|
17
|
+
|
18
|
+
yt.options = ['--output-dir', doc_destination, '--readme', 'README.rdoc']
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
desc "Generate YARD Documentation"
|
22
|
+
task :generate do
|
23
|
+
abort "Please install the YARD gem to generate rdoc."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Remove generated documenation"
|
28
|
+
task :clean do
|
29
|
+
rm_r doc_destination if File.exists?(doc_destination)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
desc 'Default: run specs.'
|
4
|
+
task :default => :spec
|
5
|
+
|
6
|
+
desc "Run specs"
|
7
|
+
RSpec::Core::RakeTask.new do |t|
|
8
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
9
|
+
# Put spec opts in a file named .rspec in root
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Generate code coverage"
|
13
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
14
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
15
|
+
t.rcov = true
|
16
|
+
t.rcov_opts = ['--exclude', '/gems/,/Library/,/usr/,spec,lib/tasks']
|
17
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'confstruct/configuration'
|
3
|
+
|
4
|
+
describe Confstruct::Configuration do
|
5
|
+
|
6
|
+
it "should initialize empty" do
|
7
|
+
conf = Confstruct::Configuration.new
|
8
|
+
conf.is_a?(Hash).should be_true
|
9
|
+
conf.is_a?(Confstruct::Configuration).should be_true
|
10
|
+
conf.should == {}
|
11
|
+
end
|
12
|
+
|
13
|
+
context "default values" do
|
14
|
+
before :all do
|
15
|
+
@defaults = {
|
16
|
+
:project => 'confstruct',
|
17
|
+
:github => {
|
18
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
19
|
+
:branch => 'master'
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
before :each do
|
25
|
+
@config = Confstruct::Configuration.new(@defaults)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have the correct defaults" do
|
29
|
+
@config.default_values.should == @defaults
|
30
|
+
@config.should == @defaults
|
31
|
+
end
|
32
|
+
|
33
|
+
it "can be defined in block mode" do
|
34
|
+
config = Confstruct::Configuration.new do
|
35
|
+
project 'confstruct'
|
36
|
+
github do
|
37
|
+
url 'http://www.github.com/mbklein/confstruct'
|
38
|
+
branch 'master'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
config.default_values.should == @defaults
|
42
|
+
config.should == @defaults
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "configuration" do
|
47
|
+
before :all do
|
48
|
+
@defaults = {
|
49
|
+
:project => 'confstruct',
|
50
|
+
:github => {
|
51
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
52
|
+
:branch => 'master'
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
@configured = {
|
57
|
+
:project => 'other-project',
|
58
|
+
:github => {
|
59
|
+
:url => 'http://www.github.com/mbklein/other-project',
|
60
|
+
:branch => 'master'
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
before :each do
|
66
|
+
@config = Confstruct::Configuration.new(@defaults)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should deep merge a hash" do
|
70
|
+
@config.configure({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
|
71
|
+
@config.should == @configured
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should configure as a struct" do
|
75
|
+
@config.project = 'other-project'
|
76
|
+
@config.github.url = 'http://www.github.com/mbklein/other-project'
|
77
|
+
@config.should == @configured
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should configure as a block" do
|
81
|
+
@config.configure do
|
82
|
+
project 'other-project'
|
83
|
+
github do
|
84
|
+
url 'http://www.github.com/mbklein/other-project'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
@config.should == @configured
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should save and restore state via #push! and #pop!" do
|
91
|
+
@config.push!({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
|
92
|
+
@configured.each_pair { |k,v| @config[k].should == v }
|
93
|
+
@config.pop!
|
94
|
+
@defaults.each_pair { |k,v| @config[k].should == v }
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should raise an exception when popping an empty stash" do
|
98
|
+
lambda { @config.pop! }.should raise_error(IndexError)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should #reset_defaults!" do
|
102
|
+
@config.project = 'other-project'
|
103
|
+
@config.github.url = 'http://www.github.com/mbklein/other-project'
|
104
|
+
@config.should == @configured
|
105
|
+
@config.reset_defaults!
|
106
|
+
@config.should == @defaults
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should call #after_config! when configuration is complete" do
|
110
|
+
postconfigurator = RSpec::Mocks::Mock.new('after_config!')
|
111
|
+
postconfigurator.should_receive(:configured!).once.with(@config)
|
112
|
+
def @config.after_config! obj
|
113
|
+
obj.project.should == 'other-project'
|
114
|
+
obj.mock.configured!(obj)
|
115
|
+
end
|
116
|
+
@config.configure do
|
117
|
+
project 'other-project'
|
118
|
+
mock postconfigurator
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'confstruct/hash_with_struct_access'
|
3
|
+
|
4
|
+
describe Confstruct::HashWithStructAccess do
|
5
|
+
|
6
|
+
it "should initialize empty" do
|
7
|
+
hwsa = Confstruct::HashWithStructAccess.new
|
8
|
+
hwsa.is_a?(Hash).should be_true
|
9
|
+
hwsa.is_a?(Confstruct::HashWithStructAccess).should be_true
|
10
|
+
hwsa.should == {}
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should respond to #ordered?" do
|
14
|
+
hwsa = Confstruct::HashWithStructAccess.new
|
15
|
+
[true,false].should include(hwsa.ordered?)
|
16
|
+
end
|
17
|
+
|
18
|
+
context "data manipulation" do
|
19
|
+
before :all do
|
20
|
+
@hash = {
|
21
|
+
:project => 'confstruct',
|
22
|
+
:github => {
|
23
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
24
|
+
:default_branch => 'master'
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
before :each do
|
30
|
+
@hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should initialize from a hash" do
|
34
|
+
hwsa = Confstruct::HashWithStructAccess.from_hash({
|
35
|
+
'project' => 'confstruct',
|
36
|
+
:github => {
|
37
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
38
|
+
'default branch' => 'master'
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
hwsa.should == @hwsa
|
43
|
+
hwsa.should == @hash
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should provide hash access" do
|
47
|
+
@hwsa[:project].should == @hash[:project]
|
48
|
+
@hwsa['project'].should == @hash[:project]
|
49
|
+
@hwsa[:github].should == @hash[:github]
|
50
|
+
@hwsa[:github][:url].should == @hash[:github][:url]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should provide struct access" do
|
54
|
+
@hwsa.project.should == @hash[:project]
|
55
|
+
@hwsa.github.should == @hash[:github]
|
56
|
+
@hwsa.github.url.should == @hash[:github][:url]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should provide block access" do
|
60
|
+
u = @hash[:github][:url]
|
61
|
+
@hwsa.github do
|
62
|
+
url.should == u
|
63
|
+
end
|
64
|
+
|
65
|
+
@hwsa.github do |g|
|
66
|
+
g.url.should == @hash[:github][:url]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should properly respond to #has?" do
|
71
|
+
@hwsa.has?('github.url').should be_true
|
72
|
+
@hwsa.has?('github.foo.bar.baz').should be_false
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should properly respond to #lookup!" do
|
76
|
+
@hwsa.lookup!('github.url').should == @hash[:github][:url]
|
77
|
+
@hwsa.lookup!('github.foo.bar.baz').should be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should provide introspection" do
|
81
|
+
@hwsa.should_respond_to(:project)
|
82
|
+
@hash.keys.each do |m|
|
83
|
+
@hwsa.methods.should include("#{m}")
|
84
|
+
@hwsa.methods.should include("#{m}=")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should #deep_merge" do
|
89
|
+
hwsa = @hwsa.deep_merge({ :new_foo => 'bar', :github => { :default_branch => 'develop' } })
|
90
|
+
@hwsa.should == @hash
|
91
|
+
hwsa.should_not == @hwsa
|
92
|
+
hwsa.should_not == @hash
|
93
|
+
hwsa.should == {
|
94
|
+
:new_foo => 'bar',
|
95
|
+
:project => 'confstruct',
|
96
|
+
:github => {
|
97
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
98
|
+
:default_branch => 'develop'
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should #deep_merge!" do
|
104
|
+
@hwsa.deep_merge!({ :github => { :default_branch => 'develop' } })
|
105
|
+
@hwsa.should_not == @hash
|
106
|
+
@hwsa.should == {
|
107
|
+
:project => 'confstruct',
|
108
|
+
:github => {
|
109
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
110
|
+
:default_branch => 'develop'
|
111
|
+
}
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should create values on demand" do
|
116
|
+
@hwsa.github.foo = 'bar'
|
117
|
+
@hwsa.github.should == {
|
118
|
+
:foo => 'bar',
|
119
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
120
|
+
:default_branch => 'master'
|
121
|
+
}
|
122
|
+
|
123
|
+
@hwsa.baz do
|
124
|
+
quux 'default_for_quux'
|
125
|
+
end
|
126
|
+
@hwsa[:baz].should == { :quux => 'default_for_quux' }
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should replace an existing hash" do
|
130
|
+
@hwsa.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
|
131
|
+
@hwsa.github.has_key?(:default_branch).should == false
|
132
|
+
@hwsa.github.should == { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should eval_or_yield all types" do
|
136
|
+
@hwsa.github do
|
137
|
+
items([]) do
|
138
|
+
self.should == []
|
139
|
+
push 1
|
140
|
+
push 'two'
|
141
|
+
push :III
|
142
|
+
self.should == [1,'two',:III]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
@hwsa.github.items.should == [1,'two',:III]
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should fail on other method signatures" do
|
149
|
+
lambda { @hwsa.error(1, 2, 3) }.should raise_error(NoMethodError)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "Proc values as virtual methods" do
|
154
|
+
before :all do
|
155
|
+
@hash = {
|
156
|
+
:project => 'confstruct',
|
157
|
+
:github => {
|
158
|
+
:url => 'http://www.github.com/mbklein/confstruct',
|
159
|
+
:default_branch => 'master',
|
160
|
+
:regular_proc => lambda { |a,b,c| puts "#{a}: #{b} #{c}" },
|
161
|
+
:reverse_url => Confstruct.deferred { self.url.reverse },
|
162
|
+
:upcase_url => Confstruct.deferred { |c| c.url.upcase }
|
163
|
+
}
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
before :each do
|
168
|
+
@hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should only evaluate Confstruct::Deferred procs" do
|
172
|
+
@hwsa.github.regular_proc.is_a?(Proc).should be_true
|
173
|
+
@hwsa.github.upcase_url.is_a?(Proc).should be_false
|
174
|
+
@hwsa.github.reverse_url.is_a?(Proc).should be_false
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should instance_eval the proc with no params" do
|
178
|
+
@hwsa.github.reverse_url.should == @hash[:github][:url].reverse
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should call the proc with params" do
|
182
|
+
@hwsa.github.upcase_url.should == @hash[:github][:url].upcase
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should evaluate deferreds when enumerating values" do
|
186
|
+
@hash[:github].values.should_not include(@hash[:github][:url].reverse)
|
187
|
+
@hwsa.github.values.should include(@hash[:github][:url].reverse)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should evaluate deferreds when inspecting" do
|
191
|
+
s = @hwsa.inspect
|
192
|
+
s.should =~ %r[:reverse_url=>"tcurtsfnoc/nielkbm/moc.buhtig.www//:ptth"]
|
193
|
+
s.should =~ %r[:regular_proc=>#<Proc:]
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should allow definition of deferreds in block mode" do
|
197
|
+
@hwsa.github do
|
198
|
+
defproc deferred! { reverse_url + upcase_url }
|
199
|
+
regproc lambda { reverse_url + upcase_url }
|
200
|
+
end
|
201
|
+
@hwsa.github.defproc.is_a?(Proc).should be_false
|
202
|
+
@hwsa.github.defproc.should == @hwsa.github.reverse_url + @hwsa.github.upcase_url
|
203
|
+
@hwsa.github.regproc.is_a?(Proc).should be_true
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should create arrays on the fly" do
|
207
|
+
@hwsa.github do
|
208
|
+
add_roles!({:jeeves => :valet}, {:wooster => :dolt})
|
209
|
+
add_roles! do
|
210
|
+
psmith :chum
|
211
|
+
end
|
212
|
+
end
|
213
|
+
@hwsa.github.roles.should == [{:jeeves => :valet}, {:wooster => :dolt}, {:psmith => :chum}]
|
214
|
+
@hwsa.github.roles.first.jeeves.should == :valet
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should not allow #add!ing to non-Array types" do
|
218
|
+
lambda {
|
219
|
+
@hwsa.github do
|
220
|
+
add_url! 'https://github.com/mbklein/busted'
|
221
|
+
end
|
222
|
+
}.should raise_error(TypeError)
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'confstruct/utils'
|
3
|
+
|
4
|
+
describe "Kernel.eval_or_yield" do
|
5
|
+
before :all do
|
6
|
+
@obj = RSpec::Mocks::Mock.new('obj')
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should instance_eval when the block takes no params" do
|
10
|
+
@obj.should_receive(:test).and_return('OK')
|
11
|
+
eval_or_yield(@obj) {
|
12
|
+
self.should_not == @obj
|
13
|
+
self.test.should == 'OK'
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should yield when the block takes a param" do
|
18
|
+
@obj.should_receive(:test).and_return('OK')
|
19
|
+
eval_or_yield(@obj) { |o|
|
20
|
+
self.should_not == @obj
|
21
|
+
o.should == @obj
|
22
|
+
lambda { self.test }.should raise_error(NoMethodError)
|
23
|
+
o.test.should == 'OK'
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return the object when no block is given" do
|
28
|
+
eval_or_yield(@obj).should == @obj
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'rspec'
|
6
|
+
require 'rspec/autorun'
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'confstruct'
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: confstruct
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Michael Klein
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-14 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rake
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 49
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 8
|
32
|
+
- 7
|
33
|
+
version: 0.8.7
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rcov
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: rdiscount
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: rdoc
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
prerelease: false
|
81
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id005
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: yard
|
94
|
+
prerelease: false
|
95
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
hash: 3
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
type: :development
|
105
|
+
version_requirements: *id006
|
106
|
+
description: A simple, hash/struct-based configuration object
|
107
|
+
email:
|
108
|
+
- mbklein@gmail.com
|
109
|
+
executables: []
|
110
|
+
|
111
|
+
extensions: []
|
112
|
+
|
113
|
+
extra_rdoc_files: []
|
114
|
+
|
115
|
+
files:
|
116
|
+
- .gitignore
|
117
|
+
- Gemfile
|
118
|
+
- README.md
|
119
|
+
- Rakefile
|
120
|
+
- confstruct.gemspec
|
121
|
+
- lib/confstruct.rb
|
122
|
+
- lib/confstruct/configuration.rb
|
123
|
+
- lib/confstruct/hash_with_struct_access.rb
|
124
|
+
- lib/confstruct/utils.rb
|
125
|
+
- lib/tasks/rdoc.rake
|
126
|
+
- lib/tasks/rspec.rake
|
127
|
+
- spec/confstruct/configuration_spec.rb
|
128
|
+
- spec/confstruct/hash_with_struct_access_spec.rb
|
129
|
+
- spec/confstruct/utils_spec.rb
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
homepage: ""
|
132
|
+
licenses: []
|
133
|
+
|
134
|
+
post_install_message:
|
135
|
+
rdoc_options: []
|
136
|
+
|
137
|
+
require_paths:
|
138
|
+
- lib
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
none: false
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
hash: 3
|
145
|
+
segments:
|
146
|
+
- 0
|
147
|
+
version: "0"
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
hash: 3
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
version: "0"
|
157
|
+
requirements: []
|
158
|
+
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 1.8.6
|
161
|
+
signing_key:
|
162
|
+
specification_version: 3
|
163
|
+
summary: A simple, hash/struct-based configuration object
|
164
|
+
test_files:
|
165
|
+
- spec/confstruct/configuration_spec.rb
|
166
|
+
- spec/confstruct/hash_with_struct_access_spec.rb
|
167
|
+
- spec/confstruct/utils_spec.rb
|
168
|
+
- spec/spec_helper.rb
|
169
|
+
has_rdoc:
|