radamanthus-skates 0.3.5
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/LICENSE +20 -0
- data/README.rdoc +113 -0
- data/Rakefile +142 -0
- data/bin/skates +8 -0
- data/lib/skates.rb +154 -0
- data/lib/skates/base/controller.rb +116 -0
- data/lib/skates/base/stanza.rb +44 -0
- data/lib/skates/base/view.rb +59 -0
- data/lib/skates/client_connection.rb +234 -0
- data/lib/skates/component_connection.rb +114 -0
- data/lib/skates/ext/array.rb +21 -0
- data/lib/skates/generator.rb +142 -0
- data/lib/skates/router.rb +123 -0
- data/lib/skates/router/dsl.rb +48 -0
- data/lib/skates/runner.rb +164 -0
- data/lib/skates/xmpp_connection.rb +216 -0
- data/lib/skates/xmpp_parser.rb +112 -0
- data/spec/bin/skates_spec.rb +0 -0
- data/spec/em_mock.rb +42 -0
- data/spec/lib/skates/base/controller_spec.rb +205 -0
- data/spec/lib/skates/base/stanza_spec.rb +120 -0
- data/spec/lib/skates/base/view_spec.rb +105 -0
- data/spec/lib/skates/client_connection_spec.rb +309 -0
- data/spec/lib/skates/component_connection_spec.rb +144 -0
- data/spec/lib/skates/generator_spec.rb +10 -0
- data/spec/lib/skates/router/dsl_spec.rb +46 -0
- data/spec/lib/skates/router_spec.rb +252 -0
- data/spec/lib/skates/runner_spec.rb +233 -0
- data/spec/lib/skates/xmpp_connection_spec.rb +222 -0
- data/spec/lib/skates/xmpp_parser_spec.rb +283 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +37 -0
- data/test/skates_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +125 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
class Array
|
2
|
+
def in_groups_of(number, fill_with = nil)
|
3
|
+
if fill_with == false
|
4
|
+
collection = self
|
5
|
+
else
|
6
|
+
# size % number gives how many extra we have;
|
7
|
+
# subtracting from number gives how many to add;
|
8
|
+
# modulo number ensures we don't add group of just fill.
|
9
|
+
padding = (number - size % number) % number
|
10
|
+
collection = dup.concat([fill_with] * padding)
|
11
|
+
end
|
12
|
+
|
13
|
+
if block_given?
|
14
|
+
collection.each_slice(number) { |slice| yield(slice) }
|
15
|
+
else
|
16
|
+
returning [] do |groups|
|
17
|
+
collection.each_slice(number) { |group| groups << group }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Skates
|
2
|
+
module Generator
|
3
|
+
extend Templater::Manifold
|
4
|
+
|
5
|
+
desc <<-DESC
|
6
|
+
Skates is a framework to generate XMPP Applications in Ruby.
|
7
|
+
DESC
|
8
|
+
|
9
|
+
##
|
10
|
+
# Generates a Skates Application
|
11
|
+
class ApplicationGenerator < Templater::Generator
|
12
|
+
desc <<-DESC
|
13
|
+
Generates the file architecture for a Skates Application. To run, you MUST provide an application name"
|
14
|
+
DESC
|
15
|
+
|
16
|
+
first_argument :application_name, :required => true, :desc => "Your application name."
|
17
|
+
|
18
|
+
def self.source_root
|
19
|
+
File.join(File.dirname(__FILE__), '../../templates/skates')
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create all subsdirectories
|
23
|
+
empty_directory :app_directory do |d|
|
24
|
+
d.destination = "#{application_name}/app"
|
25
|
+
end
|
26
|
+
empty_directory :controllers_directory do |d|
|
27
|
+
d.destination = "#{application_name}/app/controllers"
|
28
|
+
end
|
29
|
+
empty_directory :views_directory do |d|
|
30
|
+
d.destination = "#{application_name}/app/views"
|
31
|
+
end
|
32
|
+
empty_directory :views_directory do |d|
|
33
|
+
d.destination = "#{application_name}/app/stanzas"
|
34
|
+
end
|
35
|
+
empty_directory :models_directory do |d|
|
36
|
+
d.destination = "#{application_name}/app/models"
|
37
|
+
end
|
38
|
+
empty_directory :initializers_directory do |d|
|
39
|
+
d.destination = "#{application_name}/config/initializers"
|
40
|
+
end
|
41
|
+
empty_directory :destructors_directory do |d|
|
42
|
+
d.destination = "#{application_name}/config/destructors"
|
43
|
+
end
|
44
|
+
empty_directory :tmp_directory do |d|
|
45
|
+
d.destination = "#{application_name}/tmp"
|
46
|
+
end
|
47
|
+
empty_directory :log_directory do |d|
|
48
|
+
d.destination = "#{application_name}/log"
|
49
|
+
end
|
50
|
+
empty_directory :pid_directory do |d|
|
51
|
+
d.destination = "#{application_name}/tmp/pids"
|
52
|
+
end
|
53
|
+
|
54
|
+
# And now add the critical files
|
55
|
+
file :boot_file do |f|
|
56
|
+
f.source = "#{source_root}/config/boot.rb"
|
57
|
+
f.destination = "#{application_name}/config/boot.rb"
|
58
|
+
end
|
59
|
+
file :config_file do |f|
|
60
|
+
f.source = "#{source_root}/config/config.yaml"
|
61
|
+
f.destination = "#{application_name}/config/config.yaml"
|
62
|
+
end
|
63
|
+
file :dependencies_file do |f|
|
64
|
+
f.source = "#{source_root}/config/dependencies.rb"
|
65
|
+
f.destination = "#{application_name}/config/dependencies.rb"
|
66
|
+
end
|
67
|
+
file :dependencies_file do |f|
|
68
|
+
f.source = "#{source_root}/config/routes.rb"
|
69
|
+
f.destination = "#{application_name}/config/routes.rb"
|
70
|
+
end
|
71
|
+
template :component_file do |f|
|
72
|
+
f.source = "#{source_root}/script/component"
|
73
|
+
f.destination = "#{application_name}/script/component"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Generates a new controller, with the corresponding stanzas and routes.
|
80
|
+
class ControllerGenerator < Templater::Generator
|
81
|
+
desc <<-DESC
|
82
|
+
Generates a new controller for the current Application. It also adds the corresponding routes and actions, based on a Xpath and priority. \nSyntax: skates controller <controller_name> [<action_name>:<priority>:<xpath>],[...]"
|
83
|
+
DESC
|
84
|
+
|
85
|
+
first_argument :controller_name, :required => true, :desc => "Name of the Controller."
|
86
|
+
second_argument :actions_arg, :required => true, :as => :array, :default => [], :desc => "Actions implemented by this controller. Use the following syntax : name:priority:xpath"
|
87
|
+
|
88
|
+
def self.source_root
|
89
|
+
File.join(File.dirname(__FILE__), '../../templates/skates/app/controllers')
|
90
|
+
end
|
91
|
+
|
92
|
+
def controller_actions
|
93
|
+
@controller_actions ||= actions_arg.map { |a| a.split(":") }
|
94
|
+
end
|
95
|
+
|
96
|
+
def controller_class_name
|
97
|
+
"#{controller_name.capitalize}Controller"
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# This is a hack since Templater doesn't offer any simple way to edit files right now...
|
102
|
+
def add_route_for_actions_in_controller(actions, controller)
|
103
|
+
sentinel = "Skates.router.draw do"
|
104
|
+
router_path = "config/routes.rb"
|
105
|
+
actions.each do |action|
|
106
|
+
to_inject = "xpath(\"#{action[2]}\").to(:controller => \"#{controller}\", :action => \"#{action[0]}\").priority(#{action[1]})"
|
107
|
+
if File.exist?(router_path)
|
108
|
+
content = File.read(router_path).gsub(/(#{Regexp.escape(sentinel)})/mi){|match| "#{match}\n\t#{to_inject}"}
|
109
|
+
File.open(router_path, 'wb') { |file| file.write(content) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
template :controller do |t|
|
115
|
+
t.source = "#{source_root}/controller.rb"
|
116
|
+
t.destination = "app/controllers/#{controller_name}_controller.rb"
|
117
|
+
self.add_route_for_actions_in_controller(controller_actions, controller_name)
|
118
|
+
# This is a hack since Templater doesn't offer any simple way to write several files from one...
|
119
|
+
FileUtils.mkdir("app/views/#{controller_name}") unless File.exists?("app/views/#{controller_name}")
|
120
|
+
controller_actions.each do |action|
|
121
|
+
FileUtils.cp("#{source_root}/../views/view.rb", "app/views/#{controller_name}/#{action[0]}.xml.builder")
|
122
|
+
end
|
123
|
+
|
124
|
+
# And now, let's create the stanza files
|
125
|
+
controller_actions.each do |action|
|
126
|
+
FileUtils.cp("#{source_root}/../stanzas/stanza.rb", "app/stanzas/#{action[0]}.rb")
|
127
|
+
# We need to replace
|
128
|
+
# "class Stanza < Skates::Base::Stanza" with "class #{action[0]} < Skates::Base::Stanza"
|
129
|
+
content = File.read("app/stanzas/#{action[0]}.rb").gsub("class Stanza < Skates::Base::Stanza", "class #{action[0].capitalize} < Skates::Base::Stanza")
|
130
|
+
File.open("app/stanzas/#{action[0]}.rb", 'wb') { |file| file.write(content) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# The generators are added to the manifold, and assigned the names 'wiki' and 'blog'.
|
136
|
+
# So you can call them <script name> blog merb-blog-in-10-minutes and
|
137
|
+
# <script name> blog merb-wiki-in-10-minutes, respectively
|
138
|
+
add :application, ApplicationGenerator
|
139
|
+
add :controller, ControllerGenerator
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.dirname(__FILE__)+"/router/dsl"
|
2
|
+
|
3
|
+
module Skates
|
4
|
+
##
|
5
|
+
# Routers are in charge of sending the right stanzas to the right controllers based on user defined Routes.
|
6
|
+
# Each application can have only one!
|
7
|
+
class StanzaRouter
|
8
|
+
|
9
|
+
attr_reader :routes, :connection
|
10
|
+
|
11
|
+
def before_route(&block)
|
12
|
+
@before_route = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@routes = []
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Connected is called by the XmppConnection to indicate that the XMPP connection has been established
|
21
|
+
def connected(connection)
|
22
|
+
@connection = connection
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Look for the first matching route and calls the corresponding action for the corresponding controller.
|
27
|
+
# Sends the response on the XMPP stream/
|
28
|
+
# If the before_route callback is defined, it is called.
|
29
|
+
# If the callback returns true, then, the route is NOT executed.
|
30
|
+
# This callback is very useful when an application wants to redirect any stanza it receives before handling it to the routing mechanism.
|
31
|
+
def route(xml_stanza)
|
32
|
+
abort = if !@before_route.nil?
|
33
|
+
begin
|
34
|
+
@before_route.call(xml_stanza)
|
35
|
+
rescue
|
36
|
+
Skates.logger.info {
|
37
|
+
"Failed to execute before_route callback. Resuming Routing"
|
38
|
+
}
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
if !abort
|
43
|
+
route = routes.select{ |r| r.accepts?(xml_stanza) }.first
|
44
|
+
return false unless route
|
45
|
+
execute_route(route.controller, route.action, xml_stanza)
|
46
|
+
else
|
47
|
+
# The callback triggered abortion of teh routing mechanism.
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Executes the route for the given xml_stanza, by instantiating the controller_name, calling action_name and sending
|
54
|
+
# the result to the connection
|
55
|
+
def execute_route(controller_name, action_name, stanza = nil)
|
56
|
+
begin
|
57
|
+
stanza = Kernel.const_get(action_name.capitalize).new(stanza) if stanza
|
58
|
+
Skates.logger.info {
|
59
|
+
"ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
|
60
|
+
}
|
61
|
+
rescue NameError
|
62
|
+
Skates.logger.info {
|
63
|
+
"ROUTING TO #{controller_name}::#{action_name} with #{stanza.class}"
|
64
|
+
}
|
65
|
+
end
|
66
|
+
controller = controller_name.new(stanza)
|
67
|
+
controller.perform(action_name)
|
68
|
+
connection.send_xml(controller.evaluate)
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Throw away all added routes from this router. Helpful for
|
73
|
+
# testing.
|
74
|
+
def purge_routes!
|
75
|
+
@routes = []
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Run the router DSL.
|
80
|
+
def draw(&block)
|
81
|
+
dsl = Router::DSL.new
|
82
|
+
dsl.instance_eval(&block)
|
83
|
+
dsl.routes.each do |route|
|
84
|
+
raise("Route lacks destination: #{route.inspect}") unless route.is_a?(Route)
|
85
|
+
end
|
86
|
+
@routes += dsl.routes
|
87
|
+
sort
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def sort
|
93
|
+
@routes.sort! { |r1,r2| r2.priority <=> r1.priority }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Route class which associate an XPATH match and a priority to a controller and an action
|
99
|
+
class Route
|
100
|
+
|
101
|
+
attr_accessor :priority, :controller, :action, :xpath
|
102
|
+
|
103
|
+
##
|
104
|
+
# Creates a new route
|
105
|
+
def initialize(params)
|
106
|
+
raise("No controller given for route") unless params["controller"]
|
107
|
+
raise("No action given for route") unless params["action"]
|
108
|
+
raise("No xpath given for route") unless params["xpath"]
|
109
|
+
@priority = params["priority"] || 0
|
110
|
+
@xpath = params["xpath"]
|
111
|
+
@controller = Kernel.const_get("#{params["controller"].capitalize}Controller")
|
112
|
+
@action = params["action"]
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Checks that the route matches the stanzas and calls the the action on the controller.
|
117
|
+
def accepts?(stanza)
|
118
|
+
stanza.xpath(*@xpath).empty? ? false : self
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Skates
|
2
|
+
module Router
|
3
|
+
##
|
4
|
+
# We use this class to assert the ordering of the router DSL
|
5
|
+
class OutOfOrder < StandardError; end
|
6
|
+
|
7
|
+
# Creates a simple DSL for stanza routing.
|
8
|
+
class DSL
|
9
|
+
attr_reader :routes
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@routes = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Match an xpath.
|
16
|
+
def xpath(path, namespaces = {})
|
17
|
+
@routes << {"xpath" => [path, namespaces]}
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the priority of the last created route.
|
22
|
+
def priority(n)
|
23
|
+
route = @routes.last
|
24
|
+
raise OutOfOrder unless route.is_a?(Route) # check that this is in the right order
|
25
|
+
route.priority = n
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Map a route to a specific controller and action.
|
30
|
+
def to(params)
|
31
|
+
last = @routes.pop
|
32
|
+
last["controller"] = params[:controller]
|
33
|
+
last["action"] = params[:action]
|
34
|
+
# We now have all the properties we really need to create a route.
|
35
|
+
@routes << Route.new(last)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
def disco_for(type, node = nil)
|
41
|
+
str = "//iq[@type='get']/*[namespace(., 'query', 'http://jabber.org/protocol/disco##{type.to_s}')"
|
42
|
+
str << " and @node = '#{node}'" if node
|
43
|
+
str << "]"
|
44
|
+
xpath(str)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Skates
|
2
|
+
|
3
|
+
##
|
4
|
+
# Runner is in charge of running the application.
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
PHI = ((1+Math.sqrt(5))/2.0)
|
8
|
+
|
9
|
+
##
|
10
|
+
# Prepares the Application to run.
|
11
|
+
def self.prepare(env)
|
12
|
+
# Load the configuration
|
13
|
+
Skates.config = YAML.load_file(Skates.config_file)[Skates.environment]
|
14
|
+
|
15
|
+
Skates.reopen_logs
|
16
|
+
|
17
|
+
# Requiring all models, stanza, controllers
|
18
|
+
['app/models/*.rb', 'app/stanzas/*.rb', 'app/controllers/*_controller.rb'].each do |dir|
|
19
|
+
Runner.require_directory(dir)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create the router
|
23
|
+
Skates.router = Skates::StanzaRouter.new
|
24
|
+
|
25
|
+
# Evaluate routes defined with the new DSL router.
|
26
|
+
require 'config/routes.rb'
|
27
|
+
|
28
|
+
# Caching views
|
29
|
+
Skates.cache_views
|
30
|
+
|
31
|
+
#Setting failed connection attemts
|
32
|
+
@failed_connections = 0
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Convenience method to require files in a given directory
|
38
|
+
def self.require_directory(path)
|
39
|
+
Dir.glob(path).each { |f| require f }
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# When run is called, it loads the configuration, the routes and add them into the router
|
44
|
+
# It then loads the models.
|
45
|
+
# Finally it starts the EventMachine and connect the ComponentConnection
|
46
|
+
# You can pass an additional block that will be called upon launching, when the eventmachine has been started.
|
47
|
+
def self.run(env)
|
48
|
+
|
49
|
+
Skates.environment = env
|
50
|
+
|
51
|
+
# Starting the EventMachine
|
52
|
+
EventMachine.epoll
|
53
|
+
EventMachine.run do
|
54
|
+
|
55
|
+
Runner.prepare(env)
|
56
|
+
|
57
|
+
case Skates.config["application_type"]
|
58
|
+
when "client"
|
59
|
+
Skates::ClientConnection.connect(Skates.config, self)
|
60
|
+
else # By default, we assume it's a component
|
61
|
+
Skates::ComponentConnection.connect(Skates.config, self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# And finally, let's allow the application to do all it wants to do after we started the EventMachine!
|
65
|
+
yield(self) if block_given?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Returns the list of connection observers
|
71
|
+
def self.connection_observers()
|
72
|
+
@@observers ||= Array.new
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Adding a connection observer. These observer will receive on_connected and on_disconnected events.
|
77
|
+
def self.add_connection_observer(observer)
|
78
|
+
@@observers ||= Array.new
|
79
|
+
if observer.ancestors.include? Skates::Base::Controller
|
80
|
+
Skates.logger.debug {
|
81
|
+
"Added #{observer} to the list of Connection Observers"
|
82
|
+
}
|
83
|
+
@@observers.push(observer) unless @@observers.include? observer
|
84
|
+
else
|
85
|
+
Skates.logger.error {
|
86
|
+
"Observer can only be Skates::Base::Controller"
|
87
|
+
}
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Will be called by the connection class once it is connected to the server.
|
94
|
+
# It "plugs" the router and then calls on_connected on the various observers.
|
95
|
+
def self.on_connected(connection)
|
96
|
+
Skates.router.connected(connection)
|
97
|
+
connection_observers.each do |observer|
|
98
|
+
Skates.router.execute_route(observer, "on_connected")
|
99
|
+
end
|
100
|
+
|
101
|
+
# Connected so reset failed connection attempts
|
102
|
+
@failed_connections = 0
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Will be called by the connection class upon disconnection.
|
107
|
+
# It stops the event loop and then calls on_disconnected on the various observers.
|
108
|
+
def self.on_disconnected()
|
109
|
+
connection_observers.each do |conn_obs|
|
110
|
+
observer = conn_obs.new
|
111
|
+
observer.on_disconnected if observer.respond_to?("on_disconnected")
|
112
|
+
end
|
113
|
+
|
114
|
+
if Skates.config["auto-reconnect"]
|
115
|
+
# Increment failed connection attempts and calculate time to next re-connect
|
116
|
+
@failed_connections += 1
|
117
|
+
reconnect_in = fib(@failed_connections)
|
118
|
+
|
119
|
+
EventMachine.add_timer( reconnect_in ) {reconnect} if EM.reactor_running?
|
120
|
+
|
121
|
+
Skates.logger.error {
|
122
|
+
"Disconnected - trying to reconnect in #{reconnect_in} seconds."
|
123
|
+
}
|
124
|
+
else
|
125
|
+
EM.stop_event_loop
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Will be called by the connection class when it receives and parses a stanza.
|
131
|
+
def self.on_stanza(stanza)
|
132
|
+
begin
|
133
|
+
Skates.router.route(stanza)
|
134
|
+
rescue Skates::NotConnected
|
135
|
+
Skates.logger.fatal {
|
136
|
+
"#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
|
137
|
+
}
|
138
|
+
EventMachine::stop_event_loop
|
139
|
+
rescue
|
140
|
+
Skates.logger.error {
|
141
|
+
"#{$!.class} => #{$!.inspect}\n#{$!.backtrace.join("\n")}"
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Tries to reconnect
|
148
|
+
def self.reconnect
|
149
|
+
#Try to reconnect
|
150
|
+
case Skates.config["application_type"]
|
151
|
+
when "client"
|
152
|
+
Skates::ClientConnection.connect(Skates.config, self)
|
153
|
+
else # By default, we assume it's a component
|
154
|
+
Skates::ComponentConnection.connect(Skates.config, self)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Helper to calculate the fibonnacci number.
|
160
|
+
def self.fib(n)
|
161
|
+
(Skates::Runner::PHI**n).round
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|