makitzo 0.1.2
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.
- data/Gemfile +12 -0
- data/Gemfile.lock +27 -0
- data/LICENSE.txt +20 -0
- data/README.mdown +22 -0
- data/RESOURCES +2 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/makitzo +6 -0
- data/lib/makitzo/application.rb +151 -0
- data/lib/makitzo/application_aware.rb +19 -0
- data/lib/makitzo/cli.rb +71 -0
- data/lib/makitzo/config.rb +148 -0
- data/lib/makitzo/file_system.rb +24 -0
- data/lib/makitzo/logging/blackhole.rb +16 -0
- data/lib/makitzo/logging/collector.rb +170 -0
- data/lib/makitzo/logging/colorize.rb +38 -0
- data/lib/makitzo/memoized_proc.rb +15 -0
- data/lib/makitzo/migrations/commands.rb +11 -0
- data/lib/makitzo/migrations/generator.rb +24 -0
- data/lib/makitzo/migrations/migration.rb +69 -0
- data/lib/makitzo/migrations/migrator.rb +87 -0
- data/lib/makitzo/migrations/paths.rb +7 -0
- data/lib/makitzo/monkeys/array.rb +10 -0
- data/lib/makitzo/monkeys/bangify.rb +15 -0
- data/lib/makitzo/monkeys/net-ssh.rb +73 -0
- data/lib/makitzo/monkeys/string.rb +15 -0
- data/lib/makitzo/multiplexed_reader.rb +26 -0
- data/lib/makitzo/settings.rb +30 -0
- data/lib/makitzo/ssh/commands/apple.rb +104 -0
- data/lib/makitzo/ssh/commands/file_system.rb +59 -0
- data/lib/makitzo/ssh/commands/file_transfer.rb +9 -0
- data/lib/makitzo/ssh/commands/http.rb +51 -0
- data/lib/makitzo/ssh/commands/makitzo.rb +46 -0
- data/lib/makitzo/ssh/commands/ruby.rb +18 -0
- data/lib/makitzo/ssh/commands/unix.rb +7 -0
- data/lib/makitzo/ssh/context.rb +91 -0
- data/lib/makitzo/ssh/multi.rb +79 -0
- data/lib/makitzo/store/mysql.rb +176 -0
- data/lib/makitzo/store/skeleton.rb +46 -0
- data/lib/makitzo/world/host.rb +84 -0
- data/lib/makitzo/world/named_entity.rb +41 -0
- data/lib/makitzo/world/query.rb +54 -0
- data/lib/makitzo/world/role.rb +4 -0
- data/lib/makitzo.rb +90 -0
- data/makitzo.gemspec +106 -0
- data/templates/migration.erb +9 -0
- data/test/helper.rb +17 -0
- data/test/test_makitzo.rb +7 -0
- metadata +222 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
module Makitzo; module Logging
|
2
|
+
THREAD_HOST_KEY = 'makitzo.logging.host'
|
3
|
+
|
4
|
+
# A logger which collects all log messages and displays a summary by host
|
5
|
+
class Collector
|
6
|
+
include Colorize
|
7
|
+
|
8
|
+
attr_accessor :use_color
|
9
|
+
def use_color?; !!@use_color; end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@use_color = true
|
13
|
+
@host = nil
|
14
|
+
@messages = []
|
15
|
+
@hosts = Hash.new { |h,k| h[k] = {:error => false, :messages => []} }
|
16
|
+
@lock = Mutex.new
|
17
|
+
@silenced = false
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method is not threadsafe. So call it before spawning threads.
|
21
|
+
def silence(&block)
|
22
|
+
begin
|
23
|
+
was_silenced = @silenced
|
24
|
+
@silenced = true
|
25
|
+
yield if block_given?
|
26
|
+
ensure
|
27
|
+
@silenced = was_silenced
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_host(host, &block)
|
32
|
+
return unless block_given?
|
33
|
+
|
34
|
+
begin
|
35
|
+
set_current_host(host)
|
36
|
+
info("host is #{host.address}")
|
37
|
+
yield
|
38
|
+
ensure
|
39
|
+
set_current_host(nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# logs a command
|
46
|
+
# options[:command] - override command line to be logged. useful for masking passwords.
|
47
|
+
def log_command(status, options = {})
|
48
|
+
command = options[:command] || status.command
|
49
|
+
|
50
|
+
log_command_line(command, status.success?)
|
51
|
+
log_command_status(status)
|
52
|
+
|
53
|
+
overall_error! if current_host && !status.success?
|
54
|
+
end
|
55
|
+
|
56
|
+
def log_command_line(command, success = true)
|
57
|
+
if command.is_a?(Net::SSH::Connection::Session::ExecStatus)
|
58
|
+
success = command.success?
|
59
|
+
command = command.command
|
60
|
+
end
|
61
|
+
if success
|
62
|
+
append green("$", true), " ", green(sanitize(command))
|
63
|
+
else
|
64
|
+
append red("$", true), " ", red(sanitize(command))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_command_status(result, success = true)
|
69
|
+
if result.is_a?(Net::SSH::Connection::Session::ExecStatus)
|
70
|
+
success = result.success?
|
71
|
+
result = (success ? result.stdout : result.stderr).last_line.strip
|
72
|
+
end
|
73
|
+
unless result.empty?
|
74
|
+
if success
|
75
|
+
append green("-", true), " ", green(sanitize(result))
|
76
|
+
else
|
77
|
+
append red("!", true), " ", red(sanitize(result))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def overall_success!
|
83
|
+
raise "Cannot log host success when no host is set" unless current_host
|
84
|
+
@hosts[current_host.to_s][:error] = false
|
85
|
+
end
|
86
|
+
|
87
|
+
def overall_error!
|
88
|
+
raise "Cannot log host error when no host is set" unless current_host
|
89
|
+
@hosts[current_host.to_s][:error] = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def error(message)
|
93
|
+
append red("[ERROR]", true), ' ', red(sanitize(message))
|
94
|
+
overall_error! if current_host
|
95
|
+
end
|
96
|
+
|
97
|
+
def success(message)
|
98
|
+
append green("[OK]", true), ' ', sanitize(message)
|
99
|
+
end
|
100
|
+
|
101
|
+
def notice(message)
|
102
|
+
append cyan("[NOTICE]", true), ' ', sanitize(message)
|
103
|
+
end
|
104
|
+
|
105
|
+
def warn(message)
|
106
|
+
append yellow("[WARNING]", true), ' ', sanitize(message)
|
107
|
+
end
|
108
|
+
|
109
|
+
def info(message)
|
110
|
+
append '[INFO]', ' ', sanitize(message)
|
111
|
+
end
|
112
|
+
|
113
|
+
def debug(message)
|
114
|
+
append blue('[DEBUG]', true), ' ', sanitize(message)
|
115
|
+
end
|
116
|
+
|
117
|
+
def collector?
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def result
|
122
|
+
out = ""
|
123
|
+
|
124
|
+
@hosts.keys.sort.each do |host_name|
|
125
|
+
host_status = @hosts[host_name]
|
126
|
+
next if host_status[:messages].empty?
|
127
|
+
out << magenta('* ' + host_name, true) << " " << (host_status[:error] ? red('[ERROR]', true) : green('[OK]', true)) << "\n"
|
128
|
+
host_status[:messages].each { |m| out << m.indent(2) << "\n" }
|
129
|
+
out << "\n"
|
130
|
+
end
|
131
|
+
|
132
|
+
unless @messages.empty?
|
133
|
+
out << magenta("* Global Messages", true) << "\n"
|
134
|
+
@messages.each { |m| out << m.indent(2) << "\n" }
|
135
|
+
out << "\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
out
|
139
|
+
end
|
140
|
+
|
141
|
+
def append(*chunks)
|
142
|
+
unless @silenced
|
143
|
+
@lock.synchronize do
|
144
|
+
active_log << chunks.join('').strip
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def sanitize(message)
|
152
|
+
message.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
def active_log
|
156
|
+
current_host ? @hosts[current_host.to_s][:messages] : @messages
|
157
|
+
end
|
158
|
+
|
159
|
+
def current_host
|
160
|
+
Thread.current[THREAD_HOST_KEY]
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_current_host(host)
|
164
|
+
raise "Cannot set host; host already set" if host && current_host
|
165
|
+
Thread.current[THREAD_HOST_KEY] = host
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end; end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Makitzo; module Logging
|
2
|
+
module Colorize
|
3
|
+
def bold(text)
|
4
|
+
if use_color?
|
5
|
+
"\033[1m#{text}\033[0m"
|
6
|
+
else
|
7
|
+
text
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def colorize(text, ansi, bold = false)
|
12
|
+
if use_color?
|
13
|
+
code = "\033["
|
14
|
+
code << "1;" if bold
|
15
|
+
code << "#{ansi}m"
|
16
|
+
code + text + "\033[0m"
|
17
|
+
else
|
18
|
+
text
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
{ :black => 30,
|
23
|
+
:red => 31,
|
24
|
+
:green => 32,
|
25
|
+
:yellow => 33,
|
26
|
+
:blue => 34,
|
27
|
+
:magenta => 35,
|
28
|
+
:cyan => 36,
|
29
|
+
:white => 37
|
30
|
+
}.each do |color, ansi|
|
31
|
+
class_eval <<-CODE
|
32
|
+
def #{color}(text, bold = false)
|
33
|
+
colorize(text, #{ansi}, bold)
|
34
|
+
end
|
35
|
+
CODE
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end; end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Makitzo; module Migrations
|
2
|
+
class Generator
|
3
|
+
include ApplicationAware
|
4
|
+
include Paths
|
5
|
+
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_migration(name)
|
11
|
+
@migration_name = name
|
12
|
+
@migration_timestamp = Time.now.to_i
|
13
|
+
@migration_directory = File.join(local_migration_path, "#{@migration_timestamp}_#{@migration_name}")
|
14
|
+
@migration_class_name = @migration_name.camelize
|
15
|
+
|
16
|
+
template = ERB.new(File.read(File.join(Makitzo::ROOT, 'templates', 'migration.erb')))
|
17
|
+
|
18
|
+
FileUtils.mkdir_p(@migration_directory)
|
19
|
+
|
20
|
+
migration_source = template.result(binding)
|
21
|
+
File.open(File.join(@migration_directory, 'migration.rb'), 'w') { |f| f.write(migration_source) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end; end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Makitzo; module Migrations
|
2
|
+
class Migration < Makitzo::SSH::Context
|
3
|
+
class << self
|
4
|
+
def timestamp; @timestamp; end
|
5
|
+
def timestamp=(ts); @timestamp = ts; end
|
6
|
+
|
7
|
+
def directory; @directory; end
|
8
|
+
def directory=(d); @directory = d; end
|
9
|
+
|
10
|
+
def roles(*roles)
|
11
|
+
@roles ||= []
|
12
|
+
@roles.concat([roles].flatten) unless roles.empty?
|
13
|
+
@roles
|
14
|
+
end
|
15
|
+
|
16
|
+
def hosts(*hosts)
|
17
|
+
@hosts ||= []
|
18
|
+
@hosts.concat([hosts].flatten) unless hosts.empty?
|
19
|
+
@hosts
|
20
|
+
end
|
21
|
+
|
22
|
+
def query
|
23
|
+
unless @query
|
24
|
+
@query = World::Query.new
|
25
|
+
roles.each { |r| @query.roles << r }
|
26
|
+
hosts.each { |h| @query.hosts << h }
|
27
|
+
end
|
28
|
+
@query
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :role, :roles
|
32
|
+
alias_method :host, :hosts
|
33
|
+
|
34
|
+
# Returns an array of methods which are required by migrations.
|
35
|
+
# Used to prevent helpers from defining conflicting methods.
|
36
|
+
def protected_context_methods
|
37
|
+
%w(up down local_directory local_migration_file remote_directory remote_migration_file)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def local_directory
|
42
|
+
self.class.directory
|
43
|
+
end
|
44
|
+
|
45
|
+
def local_migration_file(file)
|
46
|
+
File.join(local_directory, file)
|
47
|
+
end
|
48
|
+
|
49
|
+
def remote_directory
|
50
|
+
File.join(host.migration_history_dir, self.class.timestamp.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
def remote_migration_file(file)
|
54
|
+
File.join(remote_directory, file)
|
55
|
+
end
|
56
|
+
|
57
|
+
def up
|
58
|
+
raise UnsupportedMigrationError, "up direction is not defined!"
|
59
|
+
end
|
60
|
+
|
61
|
+
def down
|
62
|
+
raise UnsupportedMigrationError, "down direction is not defined!"
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_i
|
66
|
+
self.class.timestamp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end; end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Makitzo; module Migrations
|
2
|
+
class Migrator
|
3
|
+
include ApplicationAware
|
4
|
+
include SSH::Multi
|
5
|
+
include Paths
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def migrate(target_hosts)
|
12
|
+
all_migrations = migrations
|
13
|
+
return if all_migrations.empty?
|
14
|
+
|
15
|
+
# start with a query matching all hosts and reduce to set of hosts
|
16
|
+
# affected by existing migrations
|
17
|
+
migration_hosts_query = World::Query.all
|
18
|
+
all_migrations.each { |m| migration_hosts_query.merge!(m.query) }
|
19
|
+
|
20
|
+
# get list of hosts and intersect with hosts specified on command-line
|
21
|
+
migration_hosts = migration_hosts_query.exec(config)
|
22
|
+
migration_hosts &= target_hosts
|
23
|
+
|
24
|
+
# finally, remove any hosts with no pending migrations
|
25
|
+
applied_migrations = store.applied_migrations_for_all_hosts
|
26
|
+
migration_hosts.delete_if { |host|
|
27
|
+
all_migrations.all? { |m|
|
28
|
+
(applied_migrations[host.to_s] || []).include?(m.timestamp) || !m.query.includes?(host)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
multi_ssh(migration_hosts) do |host, conn, error|
|
33
|
+
logger.with_host(host) do
|
34
|
+
if error
|
35
|
+
# log connection error
|
36
|
+
else
|
37
|
+
begin
|
38
|
+
overseer_session = ssh_context_class.new(host, conn)
|
39
|
+
overseer_session.makitzo_install_check!
|
40
|
+
|
41
|
+
# then select the appropriate migrations, create session classes and run
|
42
|
+
all_migrations.each do |migration_klass|
|
43
|
+
next if (applied_migrations[host.to_s] || []).include?(migration_klass.timestamp)
|
44
|
+
next unless migration_klass.query.includes?(host)
|
45
|
+
migration = migration_klass.new(host, conn)
|
46
|
+
migration.exec!("mkdir -p #{migration.x(migration.remote_directory)}")
|
47
|
+
migration.up
|
48
|
+
host.mark_migration_as_applied(migration)
|
49
|
+
logger.success "Migration #{migration_klass.timestamp} applied"
|
50
|
+
end
|
51
|
+
rescue SSH::CommandFailed => e
|
52
|
+
logger.error "Migration failed"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def migrations
|
62
|
+
unless @migrations
|
63
|
+
@migrations = Dir["#{local_migration_path}/*"].map do |candidate|
|
64
|
+
next unless File.directory?(candidate)
|
65
|
+
timestamp, *rest = File.basename(candidate).split('_')
|
66
|
+
class_name = rest.join('_').camelize
|
67
|
+
begin
|
68
|
+
require File.join(candidate, 'migration.rb')
|
69
|
+
klass = class_name.constantize
|
70
|
+
rescue LoadError => e
|
71
|
+
raise MigrationNotFound, "migration file not found: #{candidate}"
|
72
|
+
rescue NameError => e
|
73
|
+
raise MigrationNotFound, "expected #{candidate} to define #{class_name}"
|
74
|
+
end
|
75
|
+
klass.timestamp = timestamp.to_i
|
76
|
+
klass.directory = File.expand_path(candidate)
|
77
|
+
klass.send(:include, config.helpers)
|
78
|
+
klass
|
79
|
+
end
|
80
|
+
@migrations.compact!
|
81
|
+
@migrations.sort { |m1,m2| m1.timestamp <=> m2.timestamp }
|
82
|
+
end
|
83
|
+
@migrations
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end; end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Module
|
2
|
+
def bangify(*methods)
|
3
|
+
exception_class = 'RuntimeError'
|
4
|
+
exception_class = methods.pop if (methods.last.is_a?(Class) || methods.last =~ /^[A-Z]/)
|
5
|
+
methods.each do |method|
|
6
|
+
class_eval <<-CODE
|
7
|
+
def #{method}!(*args, &block)
|
8
|
+
result = #{method}(*args, &block)
|
9
|
+
raise #{exception_class} unless result
|
10
|
+
result
|
11
|
+
end
|
12
|
+
CODE
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class Net::SSH::Connection::Session
|
2
|
+
class ExecStatus
|
3
|
+
attr_accessor :command, :stdout, :stderr, :exit_code, :exit_signal
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@command, @stdout, @stderr, @exit_code, @exit_signal = "", "", "", 0, 0, nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def success?
|
10
|
+
@exit_code == 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def error?
|
14
|
+
!success?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_i
|
18
|
+
@exit_code
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
@stdout
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"<ExecStatus command=#{@command.inspect} stdout=#{@stdout.inspect} status=#{@exit_code}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
def hash
|
30
|
+
"#{command}\n#{exit_code}\n#{stdout}".hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def eql?(other)
|
34
|
+
other.is_a?(ExecStatus) && (command == other.command && exit_code == other.exit_code && stdout == other.stdout)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adapted from:
|
39
|
+
# http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
|
40
|
+
#
|
41
|
+
# FIXME: we're currently opening a channel per command which strikes me
|
42
|
+
# as inefficient and prevents, for example, working directory from
|
43
|
+
# persisting across requests.
|
44
|
+
def exec2!(command, options = {})
|
45
|
+
options = {:log => true}.update(options)
|
46
|
+
|
47
|
+
status = ExecStatus.new
|
48
|
+
status.command = command
|
49
|
+
|
50
|
+
ch = open_channel do |channel|
|
51
|
+
channel.exec(command) do |ch, success|
|
52
|
+
raise "could not execute command: #{command.inspect}" unless success
|
53
|
+
|
54
|
+
channel.on_data { |ch, data| status.stdout << data }
|
55
|
+
channel.on_extended_data { |ch, type, data| status.stderr << data }
|
56
|
+
channel.on_request("exit-status") { |ch,data| status.exit_code = data.read_long }
|
57
|
+
channel.on_request("exit-signal") { |ch,data| status.exit_signal = data.read_long }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
self.loop
|
62
|
+
|
63
|
+
if options[:log] && self[:logger]
|
64
|
+
if options[:log].is_a?(String)
|
65
|
+
self[:logger].log_command(status, :command => options[:log])
|
66
|
+
else
|
67
|
+
self[:logger].log_command(status)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
status
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class String
|
2
|
+
def indent(spaces)
|
3
|
+
gsub(/^/, " " * spaces)
|
4
|
+
end
|
5
|
+
|
6
|
+
# returns last non-empty line of string, or the empty string if none exists
|
7
|
+
def last_line
|
8
|
+
lines = split("\n")
|
9
|
+
while line = lines.pop
|
10
|
+
line.strip!
|
11
|
+
return line unless line.length == 0
|
12
|
+
end
|
13
|
+
return ''
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Makitzo
|
2
|
+
# relays IO from a single source to multiple threads
|
3
|
+
# each thread will see the same input.
|
4
|
+
# necessarily has to store all data as set of reader threads is not known.
|
5
|
+
# possible solution: associate reader with ThreadGroup so we can access
|
6
|
+
# list of threads. can then keep track of threads that have read least/most
|
7
|
+
# data and discard anything that's no longer needed.
|
8
|
+
class MultiplexedReader
|
9
|
+
def initialize(io)
|
10
|
+
@io, @mutex, @state, @lines = io, Mutex.new, Hash.new { |h,k| h[k] = 0 }, []
|
11
|
+
end
|
12
|
+
|
13
|
+
def gets
|
14
|
+
@mutex.synchronize do
|
15
|
+
lines_read_by_thread = @state[Thread.current]
|
16
|
+
if lines_read_by_thread >= @lines.count
|
17
|
+
@lines << @io.gets
|
18
|
+
end
|
19
|
+
@state[Thread.current] = lines_read_by_thread + 1
|
20
|
+
|
21
|
+
line = @lines[lines_read_by_thread]
|
22
|
+
line ? line.dup : line
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Makitzo
|
2
|
+
# mixin providing classes with a settings hash
|
3
|
+
module Settings
|
4
|
+
def settings
|
5
|
+
@settings ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
read(key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def []=(key, value)
|
13
|
+
set(key, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(key, default = nil)
|
17
|
+
val = settings[key.to_sym]
|
18
|
+
val = val.call if val.respond_to?(:call)
|
19
|
+
val.nil? ? default : val
|
20
|
+
end
|
21
|
+
|
22
|
+
def set(key, value = nil, &block)
|
23
|
+
settings[key.to_sym] = block_given? ? block : value
|
24
|
+
end
|
25
|
+
|
26
|
+
def memo(key, &block)
|
27
|
+
set(key, MemoizedProc.new(&block))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|