bang-bang 0.1.6
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/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +97 -0
- data/README.markdown +42 -0
- data/Rakefile +2 -0
- data/bang-bang.gemspec +38 -0
- data/lib/bang-bang/controller.rb +126 -0
- data/lib/bang-bang/env_methods.rb +13 -0
- data/lib/bang-bang/plugins/directory_first_sort.rb +14 -0
- data/lib/bang-bang/plugins.rb +29 -0
- data/lib/bang-bang/service.rb +176 -0
- data/lib/bang-bang/version.rb +3 -0
- data/lib/bang-bang/views.rb +81 -0
- data/lib/bang-bang.rb +130 -0
- data/spec/bang-bang/controller_spec.rb +36 -0
- data/spec/bang-bang/service_spec.rb +35 -0
- data/spec/bang-bang/views_spec.rb +46 -0
- data/spec/fixture-app/app.rb +30 -0
- data/spec/fixture-app/services/authentication/app/controllers/authentication.rb +6 -0
- data/spec/fixture-app/services/authentication/app/presenters/index.html.ms.rb +5 -0
- data/spec/fixture-app/services/authentication/app/templates/index.html.ms +4 -0
- data/spec/fixture-app/services/authentication/init.rb +1 -0
- data/spec/fixture-app/services/authentication/public/javascripts/foo.js +2 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/spec_helpers/example_group.rb +47 -0
- data/spec/spec_suite.rb +3 -0
- data/vendor/superhash/InstalledFiles +1 -0
- data/vendor/superhash/README +5 -0
- data/vendor/superhash/RELEASE-NOTES +9 -0
- data/vendor/superhash/config.save +12 -0
- data/vendor/superhash/examples/attributed-node.rb +83 -0
- data/vendor/superhash/examples/class-superhash.rb +26 -0
- data/vendor/superhash/examples/state-object.rb +95 -0
- data/vendor/superhash/install.rb +1015 -0
- data/vendor/superhash/lib/superhash.rb +454 -0
- data/vendor/superhash/test/test.rb +124 -0
- metadata +235 -0
data/lib/bang-bang.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
Dir[File.expand_path("#{dir}/../vendor/*/lib")].each do |path|
|
3
|
+
$LOAD_PATH.unshift(path)
|
4
|
+
end
|
5
|
+
require 'sinatra/base'
|
6
|
+
require 'mustache'
|
7
|
+
require 'yajl'
|
8
|
+
require 'active_support/all'
|
9
|
+
require 'addressable/uri'
|
10
|
+
require "named-routes"
|
11
|
+
require "superhash"
|
12
|
+
require "#{dir}/bang-bang/version"
|
13
|
+
require "#{dir}/bang-bang/env_methods"
|
14
|
+
|
15
|
+
module BangBang
|
16
|
+
def self.included(mod)
|
17
|
+
mod.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
attr_accessor :controller, :application_name, :named_routes, :stderr_dir, :stdout_dir, :root_dir, :views_class
|
22
|
+
alias_method :uris, :named_routes
|
23
|
+
delegate :define_routes, :to => :controller
|
24
|
+
|
25
|
+
include ::BangBang::EnvMethods
|
26
|
+
|
27
|
+
def init(params={})
|
28
|
+
self.controller = params[:controller] || raise(ArgumentError, "You must provide an :controller param")
|
29
|
+
self.application_name = params[:application_name] || raise(ArgumentError, "You must provide an :application_name param")
|
30
|
+
self.root_dir = params[:root_dir] || raise(ArgumentError, "You must provide a :root_dir param")
|
31
|
+
self.named_routes = params[:named_routes] || raise(ArgumentError, "You must provide a :named_routes param")
|
32
|
+
self.views_class = params[:views_class] || raise(ArgumentError, "You must provide a :views_class param")
|
33
|
+
self.controller.config = self
|
34
|
+
|
35
|
+
plugins.init
|
36
|
+
end
|
37
|
+
|
38
|
+
def app
|
39
|
+
@app ||= Rack::Builder.new do
|
40
|
+
run controller
|
41
|
+
end.to_app
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_service(path, &block)
|
45
|
+
unless service_dirs.include?(path)
|
46
|
+
service = Service.new(path)
|
47
|
+
services << service
|
48
|
+
service.init(&block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def service_dirs
|
53
|
+
services.map do |service|
|
54
|
+
service.root_dir
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def services
|
59
|
+
@services ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
def services_by_url_prefix
|
63
|
+
services.group_by do |service|
|
64
|
+
service.url_prefix
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def services_by_root_dir
|
69
|
+
services.inject({}) do |memo, service|
|
70
|
+
memo[service.root_dir] = service
|
71
|
+
memo
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def stderr_logger
|
76
|
+
@stderr_logger ||= Logger.new(stderr_dir)
|
77
|
+
end
|
78
|
+
|
79
|
+
def stdout_logger
|
80
|
+
@stdout_logger ||= Logger.new(stdout_dir)
|
81
|
+
end
|
82
|
+
|
83
|
+
def plugins
|
84
|
+
@plugins ||= ::BangBang::Plugins::Set.new(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def lib_dir
|
88
|
+
File.join(root_dir, "lib")
|
89
|
+
end
|
90
|
+
|
91
|
+
def stylesheets_dirs
|
92
|
+
service_subdirectory_dirs "app/stylesheets"
|
93
|
+
end
|
94
|
+
|
95
|
+
def vendor_dir
|
96
|
+
File.join(root_dir, "vendor")
|
97
|
+
end
|
98
|
+
|
99
|
+
def services_dir
|
100
|
+
File.join(root_dir, "services")
|
101
|
+
end
|
102
|
+
|
103
|
+
def service_subdirectory_dirs(relative_directory)
|
104
|
+
service_dirs.flatten.map do |service_path|
|
105
|
+
full_path = File.join(service_path, relative_directory)
|
106
|
+
full_path if File.directory?(full_path)
|
107
|
+
end.compact
|
108
|
+
end
|
109
|
+
|
110
|
+
def stderr_dir
|
111
|
+
"#{root_dir}/log/#{application_name}.#{rack_env}.stderr.log"
|
112
|
+
end
|
113
|
+
|
114
|
+
def stdout_dir
|
115
|
+
"#{root_dir}/log/#{application_name}.#{rack_env}.stdout.log"
|
116
|
+
end
|
117
|
+
|
118
|
+
def remove_generated_files
|
119
|
+
Dir["#{root_dir}/**/public/**/*.generated"].each do |generated_path|
|
120
|
+
FileUtils.rm_f(File.join(File.dirname(generated_path), File.basename(generated_path, ".generated")))
|
121
|
+
FileUtils.rm_f(generated_path)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
require "#{dir}/bang-bang/controller"
|
128
|
+
require "#{dir}/bang-bang/service"
|
129
|
+
require "#{dir}/bang-bang/views"
|
130
|
+
require "#{dir}/bang-bang/plugins"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")
|
2
|
+
|
3
|
+
module BangBang
|
4
|
+
describe Controller do
|
5
|
+
describe "GET /authentication/error-page" do
|
6
|
+
context "when there is not a rack.logger" do
|
7
|
+
it "responds with a 500" do
|
8
|
+
any_instance_of(FixtureApp::Controller) do |controller|
|
9
|
+
stub.proxy(controller).env do |env|
|
10
|
+
env.delete("rack.logger")
|
11
|
+
env
|
12
|
+
end
|
13
|
+
end
|
14
|
+
get uris.authentication_error_page
|
15
|
+
|
16
|
+
response.status.should == 500
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when there is a rack.logger" do
|
21
|
+
it "responds with a 500 and logs the message" do
|
22
|
+
message = nil
|
23
|
+
any_instance_of(Logger) do |l|
|
24
|
+
stub(l).error(is_a(String)) do |*args|
|
25
|
+
message = args.first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
get uris.authentication_error_page
|
29
|
+
|
30
|
+
response.status.should == 500
|
31
|
+
message.should include("An Error")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")
|
2
|
+
|
3
|
+
module BangBang
|
4
|
+
describe Service do
|
5
|
+
describe "#get_static_file_path" do
|
6
|
+
context "when the service has a file matching the given url" do
|
7
|
+
it "returns the file path of the static file base on the Service's prefix + file path" do
|
8
|
+
authentication_path = "#{FixtureApp.root_dir}/services/authentication"
|
9
|
+
service = Service.new(authentication_path).init
|
10
|
+
service.url_prefix.should == "/authentication"
|
11
|
+
service.get_static_file_path("/authentication/javascripts/foo.js").should ==
|
12
|
+
File.join(authentication_path, "/public/javascripts/foo.js")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when the service does not have a file matching the given url" do
|
17
|
+
it "returns nil" do
|
18
|
+
authentication_path = "#{FixtureApp.root_dir}/services/authentication"
|
19
|
+
service = Service.new(authentication_path).init
|
20
|
+
service.get_static_file_path("i-dont-exist").should be_nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#templates_hash" do
|
26
|
+
it "returns a hash of all of the template files" do
|
27
|
+
authentication_path = "#{FixtureApp.root_dir}/services/authentication"
|
28
|
+
service = Service.new(authentication_path).init
|
29
|
+
|
30
|
+
hash = service.templates_hash
|
31
|
+
hash["/authentication/index.html.ms"].should == File.read(File.join(service.templates_dir, "index.html.ms"))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")
|
2
|
+
|
3
|
+
module BangBang
|
4
|
+
describe Views do
|
5
|
+
describe "#[]" do
|
6
|
+
it "returns an object with a render method, which invokes the given method name" do
|
7
|
+
view = BangBang::Views.new(Object.new)
|
8
|
+
mock(view, "true_car_makes")
|
9
|
+
|
10
|
+
view["true_car_makes"].render
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "lazily created method" do
|
14
|
+
context "when the presenter file exists" do
|
15
|
+
it "evals the presenter file (which is responsible for adding the method)" do
|
16
|
+
authentication_path = "#{FixtureApp.root_dir}/services/authentication"
|
17
|
+
service = Service.new(authentication_path).init
|
18
|
+
app_instance = Object.new
|
19
|
+
stub(app_instance).services {[service]}
|
20
|
+
stub(app_instance).config {FixtureApp}
|
21
|
+
views = Class.new(BangBang::Views).new(app_instance)
|
22
|
+
|
23
|
+
html = views["/authentication/index.html.ms"].call("param1 value")
|
24
|
+
html.should include("param1 value")
|
25
|
+
doc = Nokogiri::HTML(html)
|
26
|
+
doc.at(".child-div").should be_present
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when the presenter file does not exist" do
|
31
|
+
it "lazily creates a method that render the mustache template for the given path" do
|
32
|
+
app_instance = Object.new
|
33
|
+
app_instance.class.send(:define_method, :config) do
|
34
|
+
FixtureApp
|
35
|
+
end
|
36
|
+
views = Class.new(BangBang::Views).new(app_instance)
|
37
|
+
|
38
|
+
views.respond_to?("/user/pagelets/i-dont-exist").should be_false
|
39
|
+
views["/user/pagelets/i-dont-exist"]
|
40
|
+
views.respond_to?("/user/pagelets/i-dont-exist").should be_true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module FixtureApp
|
2
|
+
include ::BangBang
|
3
|
+
|
4
|
+
def self.app
|
5
|
+
@app ||= Rack::Builder.new do
|
6
|
+
use Rack::Logger
|
7
|
+
run ::FixtureApp::Controller
|
8
|
+
end.to_app
|
9
|
+
end
|
10
|
+
|
11
|
+
class Controller < ::BangBang::Controller
|
12
|
+
set :dump_errors, false
|
13
|
+
end
|
14
|
+
|
15
|
+
class Views < ::BangBang::Views
|
16
|
+
end
|
17
|
+
|
18
|
+
class Routes < NamedRoutes::Routes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
FixtureApp.init(
|
23
|
+
:controller => FixtureApp::Controller,
|
24
|
+
:application_name => "fixture-app",
|
25
|
+
:root_dir => File.dirname(__FILE__),
|
26
|
+
:named_routes => FixtureApp::Routes,
|
27
|
+
:views_class => FixtureApp::Views
|
28
|
+
)
|
29
|
+
|
30
|
+
FixtureApp.register_service("#{File.dirname(__FILE__)}/services/authentication")
|
@@ -0,0 +1 @@
|
|
1
|
+
self.url_prefix = "/authentication"
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
dir = File.dirname(__FILE__)
|
6
|
+
$LOAD_PATH.unshift(File.expand_path("#{dir}/../lib"))
|
7
|
+
|
8
|
+
require "rack/test"
|
9
|
+
|
10
|
+
require "bang-bang"
|
11
|
+
require "#{dir}/fixture-app/app"
|
12
|
+
require "rack/session/abstract/id"
|
13
|
+
require "rack/test"
|
14
|
+
require "capybara"
|
15
|
+
require "capybara/dsl"
|
16
|
+
require "addressable/uri"
|
17
|
+
|
18
|
+
ARGV.push("-b")
|
19
|
+
unless ARGV.include?("--format") || ARGV.include?("-f")
|
20
|
+
ARGV.push("--format", "nested")
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rspec'
|
24
|
+
require 'rspec/autorun'
|
25
|
+
require 'rr'
|
26
|
+
require 'webmock/rspec'
|
27
|
+
|
28
|
+
ENV["RACK_ENV"] = "test"
|
29
|
+
|
30
|
+
Dir["#{File.dirname(__FILE__)}/spec_helpers/**/*.rb"].each do |file|
|
31
|
+
require file
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec.configure do |configuration|
|
35
|
+
configuration.mock_with :rr
|
36
|
+
configuration.filter_run :focus => true
|
37
|
+
configuration.run_all_when_everything_filtered = true
|
38
|
+
configuration.before do
|
39
|
+
Capybara.reset!
|
40
|
+
|
41
|
+
FixtureApp.views_class = FixtureApp::Views
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Capybara.app = FixtureApp.app
|
@@ -0,0 +1,47 @@
|
|
1
|
+
RSpec::Core::ExampleGroup.class_eval do
|
2
|
+
include Capybara
|
3
|
+
|
4
|
+
def app
|
5
|
+
FixtureApp::Controller
|
6
|
+
end
|
7
|
+
|
8
|
+
def uris
|
9
|
+
@uris ||= FixtureApp::Routes.instance
|
10
|
+
end
|
11
|
+
alias_method :named_routes, :uris
|
12
|
+
|
13
|
+
def views
|
14
|
+
@views ||= begin
|
15
|
+
app_instance = Capybara.app.allocate
|
16
|
+
app_instance.send(:initialize)
|
17
|
+
app_instance.session_data = session_data
|
18
|
+
::FixtureApp::Views.new(app_instance)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
[
|
23
|
+
:request, :last_request, :response, :last_response, :follow_redirect!,
|
24
|
+
:rack_mock_session
|
25
|
+
].each do |method_name|
|
26
|
+
class_eval((<<-RUBY), __FILE__, __LINE__+1)
|
27
|
+
def #{method_name}(*args, &block)
|
28
|
+
page.driver.#{method_name}(*args, &block)
|
29
|
+
end
|
30
|
+
RUBY
|
31
|
+
end
|
32
|
+
|
33
|
+
[:get, :put, :post, :delete].each do |method_name|
|
34
|
+
class_eval((<<-RUBY), __FILE__, __LINE__+1)
|
35
|
+
def #{method_name}(*args, &block)
|
36
|
+
(res = page.driver.#{method_name}(*args, &block)).tap do
|
37
|
+
puts res.errors if res.status == 500
|
38
|
+
end
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
|
43
|
+
def current_path
|
44
|
+
Addressable::URI.parse(current_url).path
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/spec/spec_suite.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/home/honk/.rvm/rubies/ruby-1.9.2-p0/lib/ruby/site_ruby/1.9.1//superhash.rb
|
@@ -0,0 +1,12 @@
|
|
1
|
+
prefix=/home/honk/.rvm/rubies/ruby-1.9.2-p0
|
2
|
+
std-ruby=$prefix/lib/ruby/1.9.1
|
3
|
+
site-ruby-common=$prefix/lib/ruby/site_ruby
|
4
|
+
site-ruby=$prefix/lib/ruby/site_ruby/1.9.1
|
5
|
+
bin-dir=$prefix/bin
|
6
|
+
rb-dir=$site-ruby
|
7
|
+
so-dir=$prefix/lib/ruby/site_ruby/1.9.1/x86_64-linux
|
8
|
+
data-dir=$prefix/share
|
9
|
+
ruby-path=/home/honk/.rvm/rubies/ruby-1.9.2-p0/bin/ruby
|
10
|
+
ruby-prog=/home/honk/.rvm/rubies/ruby-1.9.2-p0/bin/ruby
|
11
|
+
make-prog=make
|
12
|
+
without-ext=no
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'superhash'
|
2
|
+
|
3
|
+
# Example of using SuperHashes inside regular objects (not classes).
|
4
|
+
# This example was distilled from the SuperML project.
|
5
|
+
|
6
|
+
class AttributedNode
|
7
|
+
|
8
|
+
attr_reader :attributes, :parents, :children
|
9
|
+
|
10
|
+
# creates a node and gives it a first parent
|
11
|
+
def initialize(parent = nil)
|
12
|
+
@attributes = SuperHash.new(parent && parent.attributes)
|
13
|
+
@parents = parent ? [parent] : []
|
14
|
+
@children = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# adds more parents, which will also contribute attributes
|
18
|
+
def add_parent(parent)
|
19
|
+
unless @parents.include? parent
|
20
|
+
@parents << parent
|
21
|
+
@attributes.parents << parent.attributes
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
protected :add_parent
|
26
|
+
|
27
|
+
def add_child(child)
|
28
|
+
@children << child
|
29
|
+
child.add_parent(self)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
# This might be the description of a window using some (imaginary)
|
36
|
+
# GUI construction library.
|
37
|
+
|
38
|
+
root = AttributedNode.new
|
39
|
+
root.attributes[:type] = :top_window
|
40
|
+
root.attributes[:name] = "Feature request window"
|
41
|
+
root.attributes[:bg_color] = "gray"
|
42
|
+
root.attributes[:font_face] = "Courier"
|
43
|
+
root.attributes[:font_size] = 10
|
44
|
+
|
45
|
+
user_field = AttributedNode.new
|
46
|
+
root.add_child(user_field)
|
47
|
+
user_field.attributes[:type] = :text_box
|
48
|
+
user_field.attributes[:name] = "Username box"
|
49
|
+
user_field.attributes[:label] = "Name:"
|
50
|
+
|
51
|
+
feature_field = AttributedNode.new
|
52
|
+
root.add_child(feature_field)
|
53
|
+
feature_field.attributes[:type] = :text_box
|
54
|
+
feature_field.attributes[:name] = "Feature description box"
|
55
|
+
feature_field.attributes[:bg_color] = "white"
|
56
|
+
feature_field.attributes[:label] = "Please enter your request below:"
|
57
|
+
feature_field.attributes[:font_face] = "Times"
|
58
|
+
feature_field.attributes[:font_size] = 12
|
59
|
+
|
60
|
+
|
61
|
+
# So then we have:
|
62
|
+
|
63
|
+
p user_field.attributes[:bg_color] # ==> "gray"
|
64
|
+
p feature_field.attributes[:bg_color] # ==> "white"
|
65
|
+
|
66
|
+
p user_field.attributes[:font_size] # ==> 10
|
67
|
+
p feature_field.attributes[:font_size] # ==> 12
|
68
|
+
|
69
|
+
|
70
|
+
# You could even manage another set of attributes of the same objects using
|
71
|
+
# multiple inheritance, such as attributes pertaining to form entry content:
|
72
|
+
|
73
|
+
form_entry = AttributedNode.new
|
74
|
+
form_entry.attributes[:default_contents] = "enter text here"
|
75
|
+
form_entry.attributes[:reject_condition] = /[^\w,.;:?!]/
|
76
|
+
|
77
|
+
form_entry.add_child(user_field)
|
78
|
+
user_field.attributes[:default_contents] = "fred"
|
79
|
+
|
80
|
+
form_entry.add_child(feature_field)
|
81
|
+
feature_field.attributes[:reject_condition] = /more like Microsoft/
|
82
|
+
|
83
|
+
p feature_field.attributes[:default_contents] # ==> "enter text here"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'superhash'
|
2
|
+
|
3
|
+
class A
|
4
|
+
class_superhash :options
|
5
|
+
|
6
|
+
options[:foo] = "A foo"
|
7
|
+
options[:bar] = "A bar"
|
8
|
+
|
9
|
+
def options; self.class.options; end
|
10
|
+
end
|
11
|
+
|
12
|
+
class B < A
|
13
|
+
options[:foo] = "B foo"
|
14
|
+
end
|
15
|
+
|
16
|
+
p A.options
|
17
|
+
p B.options.to_hash
|
18
|
+
p B.new.options.to_hash
|
19
|
+
|
20
|
+
__END__
|
21
|
+
|
22
|
+
output:
|
23
|
+
|
24
|
+
{:foo=>"A foo", :bar=>"A bar"}
|
25
|
+
{:foo=>"B foo", :bar=>"A bar"}
|
26
|
+
{:foo=>"B foo", :bar=>"A bar"}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'superhash'
|
2
|
+
|
3
|
+
# Example of using SuperHashes in class instance variables to extend Ruby's
|
4
|
+
# inheritance mechanism. This can be expressed automatically using the
|
5
|
+
# Class#class_superhash methods defined in superhash.rb.
|
6
|
+
#
|
7
|
+
# This example is vaguely derived from the RedShift project. Subclasses
|
8
|
+
# of StateObject have a set of states that their instances may be in. Each
|
9
|
+
# state is mapped to a state action. Subclasses inherit the mapping, and may
|
10
|
+
# add to it and override it.
|
11
|
+
|
12
|
+
class StateObject
|
13
|
+
|
14
|
+
# class vars and class methods
|
15
|
+
|
16
|
+
@state_actions = {}
|
17
|
+
# root hash for child class's superhash that maps state => action
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def state_actions
|
21
|
+
@state_actions ||= SuperHash.new(superclass.state_actions)
|
22
|
+
end
|
23
|
+
|
24
|
+
def define_state_action(state, &action)
|
25
|
+
state_actions[state] = action
|
26
|
+
end
|
27
|
+
alias in_state define_state_action
|
28
|
+
end
|
29
|
+
|
30
|
+
define_state_action :no_op # all subclasses inherit this state
|
31
|
+
|
32
|
+
# instance methods
|
33
|
+
|
34
|
+
def state_actions
|
35
|
+
self.class.state_actions
|
36
|
+
end
|
37
|
+
|
38
|
+
def states
|
39
|
+
state_actions.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :cur_state
|
43
|
+
|
44
|
+
def cur_state=(target_state)
|
45
|
+
unless states.include? target_state
|
46
|
+
raise "No way, dude. That's not a cool state for " +
|
47
|
+
"me to be in right now."
|
48
|
+
end
|
49
|
+
@cur_state = target_state
|
50
|
+
@cur_action = state_actions[target_state]
|
51
|
+
end
|
52
|
+
|
53
|
+
def run
|
54
|
+
@cur_action.call if @cur_action
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(start_state = :no_op)
|
58
|
+
self.cur_state = start_state
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Computer < StateObject
|
63
|
+
in_state :off do
|
64
|
+
puts "Power off."
|
65
|
+
end
|
66
|
+
|
67
|
+
in_state :on do
|
68
|
+
puts "Power on."
|
69
|
+
end
|
70
|
+
|
71
|
+
in_state :boot do
|
72
|
+
puts "Can't boot -- no OS."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class WindowsNTBox < Computer
|
77
|
+
# Add a new state, and its action.
|
78
|
+
in_state :crashed do
|
79
|
+
puts "Blue screen!"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Override an inherited state.
|
83
|
+
in_state :boot do
|
84
|
+
puts "Booting Windows NT."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
pc = WindowsNTBox.new
|
89
|
+
|
90
|
+
seq = [:off, :on, :boot, :crashed, :off]
|
91
|
+
|
92
|
+
for state in seq
|
93
|
+
pc.cur_state = state
|
94
|
+
pc.run
|
95
|
+
end
|