skates 0.1.11
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 +143 -0
- data/bin/skates +6 -0
- data/lib/skates.rb +108 -0
- data/lib/skates/base/controller.rb +116 -0
- data/lib/skates/base/stanza.rb +23 -0
- data/lib/skates/base/view.rb +58 -0
- data/lib/skates/client_connection.rb +210 -0
- data/lib/skates/component_connection.rb +87 -0
- data/lib/skates/generator.rb +139 -0
- data/lib/skates/router.rb +101 -0
- data/lib/skates/router/dsl.rb +61 -0
- data/lib/skates/runner.rb +137 -0
- data/lib/skates/xmpp_connection.rb +172 -0
- data/lib/skates/xmpp_parser.rb +117 -0
- data/lib/skates/xpath_helper.rb +13 -0
- data/spec/bin/babylon_spec.rb +0 -0
- data/spec/em_mock.rb +42 -0
- data/spec/lib/babylon/base/controller_spec.rb +205 -0
- data/spec/lib/babylon/base/stanza_spec.rb +15 -0
- data/spec/lib/babylon/base/view_spec.rb +92 -0
- data/spec/lib/babylon/client_connection_spec.rb +304 -0
- data/spec/lib/babylon/component_connection_spec.rb +135 -0
- data/spec/lib/babylon/generator_spec.rb +10 -0
- data/spec/lib/babylon/router/dsl_spec.rb +72 -0
- data/spec/lib/babylon/router_spec.rb +189 -0
- data/spec/lib/babylon/runner_spec.rb +213 -0
- data/spec/lib/babylon/xmpp_connection_spec.rb +197 -0
- data/spec/lib/babylon/xmpp_parser_spec.rb +275 -0
- data/spec/lib/babylon/xpath_helper_spec.rb +25 -0
- data/spec/spec_helper.rb +34 -0
- data/templates/skates/app/controllers/controller.rb +7 -0
- data/templates/skates/app/stanzas/stanza.rb +6 -0
- data/templates/skates/app/views/view.rb +6 -0
- data/templates/skates/config/boot.rb +16 -0
- data/templates/skates/config/config.yaml +24 -0
- data/templates/skates/config/dependencies.rb +1 -0
- data/templates/skates/config/routes.rb +22 -0
- data/templates/skates/log/development.log +0 -0
- data/templates/skates/log/production.log +0 -0
- data/templates/skates/log/test.log +0 -0
- data/templates/skates/script/component +36 -0
- data/templates/skates/tmp/pids/README +2 -0
- data/test/skates_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +160 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Skates
|
2
|
+
module Base
|
3
|
+
##
|
4
|
+
# Class used to Parse a Stanza on the XMPP stream.
|
5
|
+
# You should have a Stanza subsclass for each of your controller actions, as they allow you to define which stanzas and which information is passed to yoru controllers.
|
6
|
+
#
|
7
|
+
# You can define your own macthing pretty easily with the element and elements methods, as explained in the SaxMachine Documentation: http://github.com/pauldix/sax-machine/tree/master
|
8
|
+
# if your stanza is a message stanza, you can match the following for example:
|
9
|
+
# element :message, :value => :to, :as => :to
|
10
|
+
# element :message, :value => :from, :as => :from
|
11
|
+
# element :message, :value => :id, :as => :stanza_id
|
12
|
+
# element :message, :value => :type, :as => :stanza_type
|
13
|
+
# element :message, :value => :"xml:lang", :as => :lang
|
14
|
+
#
|
15
|
+
class Stanza
|
16
|
+
include SAXMachine
|
17
|
+
|
18
|
+
def initialize(xml = nil)
|
19
|
+
parse(xml.to_s)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Skates
|
2
|
+
module Base
|
3
|
+
|
4
|
+
class ViewFileNotFound < Errno::ENOENT; end
|
5
|
+
|
6
|
+
##
|
7
|
+
# Your application's views (stanzas) should be descendant of this class.
|
8
|
+
class View
|
9
|
+
attr_reader :view_template
|
10
|
+
|
11
|
+
##
|
12
|
+
# Used to 'include' another view inside an existing view.
|
13
|
+
# The caller needs to pass the context in which the partial will be rendered
|
14
|
+
# Render must be called with :partial as well (other options will be supported later). The partial vale should be a relative path
|
15
|
+
# to another file view, from the calling view.
|
16
|
+
# You can also use :locals => {:name => value} to use defined locals in your embedded views.
|
17
|
+
def render(xml, options = {})
|
18
|
+
# First, we need to identify the partial file path, based on the @view_template path.
|
19
|
+
partial_path = (@view_template.split("/")[0..-2] + options[:partial].split("/")).join("/").gsub(".xml.builder", "") + ".xml.builder"
|
20
|
+
raise ViewFileNotFound, "No such file #{partial_path}" unless Skates.views[partial_path]
|
21
|
+
saved_locals = @locals
|
22
|
+
@locals = options[:locals]
|
23
|
+
eval(Skates.views[partial_path], binding, partial_path, 1)
|
24
|
+
@locals = saved_locals # Re-assign the previous locals to be 'clean'
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Instantiate a new view with the various varibales passed in assigns and the path of the template to render.
|
29
|
+
def initialize(path = "", assigns = {})
|
30
|
+
@view_template = path
|
31
|
+
@locals = {}
|
32
|
+
assigns.each do |key, value|
|
33
|
+
instance_variable_set(:"@#{key}", value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# "Loads" the view file, and uses the Nokogiri Builder to build the XML stanzas that will be sent.
|
39
|
+
def evaluate
|
40
|
+
return if @view_template == ""
|
41
|
+
raise ViewFileNotFound, "No such file #{@view_template}" unless Skates.views[@view_template]
|
42
|
+
builder = Nokogiri::XML::Builder.new
|
43
|
+
builder.stream do |xml|
|
44
|
+
eval(Skates.views[@view_template], binding, @view_template, 1)
|
45
|
+
end
|
46
|
+
builder.doc.root.children # we output the document built
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Used to macth locals variables
|
51
|
+
def method_missing(sym, *args, &block)
|
52
|
+
raise NameError, "undefined local variable or method `#{sym}' for #{self}" unless @locals[sym]
|
53
|
+
@locals[sym]
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module Skates
|
2
|
+
|
3
|
+
##
|
4
|
+
# ClientConnection is in charge of the XMPP connection for a Regular XMPP Client.
|
5
|
+
# So far, SASL Plain authenticationonly is supported
|
6
|
+
# Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
|
7
|
+
class ClientConnection < XmppConnection
|
8
|
+
|
9
|
+
attr_reader :binding_iq_id, :session_iq_id
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a new ClientConnection and waits for data in the stream
|
13
|
+
def initialize(params)
|
14
|
+
super(params)
|
15
|
+
@state = :wait_for_stream
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Connects the ClientConnection based on SRV records for the jid's domain, if no host or port has been specified.
|
20
|
+
# In any case, we give priority to the specified host and port.
|
21
|
+
def self.connect(params, handler = nil)
|
22
|
+
return super(params, handler) if params["host"] && params["port"]
|
23
|
+
|
24
|
+
begin
|
25
|
+
srv = []
|
26
|
+
Resolv::DNS.open { |dns|
|
27
|
+
# If ruby version is too old and SRV is unknown, this will raise a NameError
|
28
|
+
# which is caught below
|
29
|
+
host_from_jid = params["jid"].split("/").first.split("@").last
|
30
|
+
Skates.logger.debug {
|
31
|
+
"RESOLVING: _xmpp-client._tcp.#{host_from_jid} (SRV)"
|
32
|
+
}
|
33
|
+
srv = dns.getresources("_xmpp-client._tcp.#{host_from_jid}", Resolv::DNS::Resource::IN::SRV)
|
34
|
+
}
|
35
|
+
# Sort SRV records: lowest priority first, highest weight first
|
36
|
+
srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
|
37
|
+
# And now, for each record, let's try to connect.
|
38
|
+
srv.each { |record|
|
39
|
+
begin
|
40
|
+
params["host"] = record.target.to_s
|
41
|
+
params["port"] = Integer(record.port)
|
42
|
+
super(params, handler)
|
43
|
+
# Success
|
44
|
+
break
|
45
|
+
rescue NotConnected
|
46
|
+
# Try next SRV record
|
47
|
+
end
|
48
|
+
}
|
49
|
+
rescue NameError
|
50
|
+
Skates.logger.debug {
|
51
|
+
"Resolv::DNS does not support SRV records. Please upgrade to ruby-1.8.3 or later! \n#{$!} : #{$!.backtrace.join("\n")}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Builds the stream stanza for this client
|
58
|
+
def stream_stanza
|
59
|
+
doc = Nokogiri::XML::Document.new
|
60
|
+
stream = Nokogiri::XML::Node.new("stream:stream", doc)
|
61
|
+
doc.add_child(stream)
|
62
|
+
stream["xmlns"] = stream_namespace
|
63
|
+
stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
|
64
|
+
stream["to"] = jid.split("/").first.split("@").last
|
65
|
+
stream["version"] = "1.0"
|
66
|
+
paste_content_here = Nokogiri::XML::Node.new("paste_content_here", doc)
|
67
|
+
stream.add_child(paste_content_here)
|
68
|
+
doc.to_xml.split('<paste_content_here/>').first
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
|
73
|
+
# to establish the XMPP connection itself.
|
74
|
+
# We use a "tweak" here to send only the starting tag of stream:stream
|
75
|
+
def connection_completed
|
76
|
+
super
|
77
|
+
send_xml(stream_stanza)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Called upon stanza reception
|
82
|
+
# Marked as connected when the client has been SASLed, authenticated, biund to a resource and when the session has been created
|
83
|
+
def receive_stanza(stanza)
|
84
|
+
case @state
|
85
|
+
when :connected
|
86
|
+
super # Can be dispatched
|
87
|
+
|
88
|
+
when :wait_for_stream_authenticated
|
89
|
+
if stanza.name == "stream:stream" && stanza.attributes['id']
|
90
|
+
@state = :wait_for_bind
|
91
|
+
end
|
92
|
+
|
93
|
+
when :wait_for_stream
|
94
|
+
if stanza.name == "stream:stream" && stanza.attributes['id']
|
95
|
+
@state = :wait_for_auth_mechanisms
|
96
|
+
end
|
97
|
+
|
98
|
+
when :wait_for_auth_mechanisms
|
99
|
+
if stanza.name == "stream:features"
|
100
|
+
if stanza.at("starttls") # we shall start tls
|
101
|
+
doc = Nokogiri::XML::Document.new
|
102
|
+
starttls = Nokogiri::XML::Node.new("starttls", doc)
|
103
|
+
doc.add_child(starttls)
|
104
|
+
starttls["xmlns"] = "urn:ietf:params:xml:ns:xmpp-tls"
|
105
|
+
send_xml(starttls.to_s)
|
106
|
+
@state = :wait_for_proceed
|
107
|
+
elsif stanza.at("mechanisms") # tls is ok
|
108
|
+
if stanza.at("mechanisms").children.map() { |m| m.text }.include? "PLAIN"
|
109
|
+
doc = Nokogiri::XML::Document.new
|
110
|
+
auth = Nokogiri::XML::Node.new("auth", doc)
|
111
|
+
doc.add_child(auth)
|
112
|
+
auth['mechanism'] = "PLAIN"
|
113
|
+
auth["xmlns"] = "urn:ietf:params:xml:ns:xmpp-sasl"
|
114
|
+
auth.content = Base64::encode64([jid, jid.split("@").first, @password].join("\000")).gsub(/\s/, '')
|
115
|
+
send_xml(auth.to_s)
|
116
|
+
@state = :wait_for_success
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
when :wait_for_success
|
122
|
+
if stanza.name == "success" # Yay! Success
|
123
|
+
@state = :wait_for_stream_authenticated
|
124
|
+
@parser.reset
|
125
|
+
send_xml(stream_stanza)
|
126
|
+
elsif stanza.name == "failure"
|
127
|
+
if stanza.at("bad-auth") || stanza.at("not-authorized")
|
128
|
+
raise AuthenticationError
|
129
|
+
else
|
130
|
+
end
|
131
|
+
else
|
132
|
+
# Hum Failure...
|
133
|
+
end
|
134
|
+
|
135
|
+
when :wait_for_bind
|
136
|
+
if stanza.name == "stream:features"
|
137
|
+
if stanza.at("bind")
|
138
|
+
doc = Nokogiri::XML::Document.new
|
139
|
+
# Let's build the binding_iq
|
140
|
+
@binding_iq_id = Integer(rand(10000000))
|
141
|
+
iq = Nokogiri::XML::Node.new("iq", doc)
|
142
|
+
doc.add_child(iq)
|
143
|
+
iq["type"] = "set"
|
144
|
+
iq["id"] = binding_iq_id.to_s
|
145
|
+
bind = Nokogiri::XML::Node.new("bind", doc)
|
146
|
+
bind["xmlns"] = "urn:ietf:params:xml:ns:xmpp-bind"
|
147
|
+
iq.add_child(bind)
|
148
|
+
resource = Nokogiri::XML::Node.new("resource", doc)
|
149
|
+
if jid.split("/").size == 2
|
150
|
+
resource.content = (@jid.split("/").last)
|
151
|
+
else
|
152
|
+
resource.content = "skates_client_#{binding_iq_id}"
|
153
|
+
end
|
154
|
+
bind.add_child(resource)
|
155
|
+
send_xml(iq.to_s)
|
156
|
+
@state = :wait_for_confirmed_binding
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
when :wait_for_confirmed_binding
|
161
|
+
if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == binding_iq_id
|
162
|
+
if stanza.at("jid")
|
163
|
+
@jid = stanza.at("jid").text
|
164
|
+
end
|
165
|
+
# And now, we must initiate the session
|
166
|
+
@session_iq_id = Integer(rand(10000))
|
167
|
+
doc = Nokogiri::XML::Document.new
|
168
|
+
iq = Nokogiri::XML::Node.new("iq", doc)
|
169
|
+
doc.add_child(iq)
|
170
|
+
iq["type"] = "set"
|
171
|
+
iq["id"] = session_iq_id.to_s
|
172
|
+
session = Nokogiri::XML::Node.new("session", doc)
|
173
|
+
session["xmlns"] = "urn:ietf:params:xml:ns:xmpp-session"
|
174
|
+
iq.add_child(session)
|
175
|
+
send_xml(iq.to_s)
|
176
|
+
@state = :wait_for_confirmed_session
|
177
|
+
end
|
178
|
+
|
179
|
+
when :wait_for_confirmed_session
|
180
|
+
if stanza.name == "iq" && stanza["type"] == "result" && Integer(stanza["id"]) == session_iq_id
|
181
|
+
# And now, send a presence!
|
182
|
+
doc = Nokogiri::XML::Document.new
|
183
|
+
presence = Nokogiri::XML::Node.new("presence", doc)
|
184
|
+
send_xml(presence.to_s)
|
185
|
+
begin
|
186
|
+
@handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
|
187
|
+
rescue
|
188
|
+
Skates.logger.error {
|
189
|
+
"on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
|
190
|
+
}
|
191
|
+
end
|
192
|
+
@state = :connected
|
193
|
+
end
|
194
|
+
|
195
|
+
when :wait_for_proceed
|
196
|
+
start_tls() # starting TLS
|
197
|
+
@state = :wait_for_stream
|
198
|
+
@parser.reset
|
199
|
+
send_xml stream_stanza
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
##
|
204
|
+
# Namespace of the client
|
205
|
+
def stream_namespace
|
206
|
+
"jabber:client"
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Skates
|
2
|
+
##
|
3
|
+
# ComponentConnection is in charge of the XMPP connection itself.
|
4
|
+
# Upon stanza reception, and depending on the status (connected... etc), this component will handle or forward the stanzas.
|
5
|
+
class ComponentConnection < XmppConnection
|
6
|
+
|
7
|
+
##
|
8
|
+
# Creates a new ComponentConnection and waits for data in the stream
|
9
|
+
def initialize(params)
|
10
|
+
super(params)
|
11
|
+
@state = :wait_for_stream
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Connection_completed is called when the connection (socket) has been established and is in charge of "building" the XML stream
|
16
|
+
# to establish the XMPP connection itself.
|
17
|
+
# We use a "tweak" here to send only the starting tag of stream:stream
|
18
|
+
def connection_completed
|
19
|
+
super
|
20
|
+
doc = Nokogiri::XML::Document.new
|
21
|
+
stream = Nokogiri::XML::Node.new("stream:stream", doc)
|
22
|
+
stream["xmlns"] = stream_namespace
|
23
|
+
stream["xmlns:stream"] = "http://etherx.jabber.org/streams"
|
24
|
+
stream["to"] = jid
|
25
|
+
doc.add_child(stream)
|
26
|
+
paste_content_here= Nokogiri::XML::Node.new("paste_content_here", doc)
|
27
|
+
stream.add_child(paste_content_here)
|
28
|
+
start, stop = doc.to_xml.split('<paste_content_here/>')
|
29
|
+
send_xml(start)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# XMPP Component handshake as defined in XEP-0114:
|
34
|
+
# http://xmpp.org/extensions/xep-0114.html
|
35
|
+
def receive_stanza(stanza)
|
36
|
+
case @state
|
37
|
+
when :connected # Most frequent case
|
38
|
+
super(stanza) # Can be dispatched
|
39
|
+
|
40
|
+
when :wait_for_stream
|
41
|
+
if stanza.name == "stream:stream" && stanza.attributes['id']
|
42
|
+
# This means the XMPP session started!
|
43
|
+
# We must send the handshake now.
|
44
|
+
send_xml(handshake(stanza))
|
45
|
+
@state = :wait_for_handshake
|
46
|
+
else
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
when :wait_for_handshake
|
51
|
+
if stanza.name == "handshake"
|
52
|
+
begin
|
53
|
+
@handler.on_connected(self) if @handler and @handler.respond_to?("on_connected")
|
54
|
+
rescue
|
55
|
+
Skates.logger.error {
|
56
|
+
"on_connected failed : #{$!}\n#{$!.backtrace.join("\n")}"
|
57
|
+
}
|
58
|
+
end
|
59
|
+
@state = :connected
|
60
|
+
elsif stanza.name == "stream:error"
|
61
|
+
raise AuthenticationError
|
62
|
+
else
|
63
|
+
raise
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Namespace of the component
|
71
|
+
def stream_namespace
|
72
|
+
'jabber:component:accept'
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def handshake(stanza)
|
78
|
+
hash = Digest::SHA1::hexdigest(stanza.attributes['id'].content + @password)
|
79
|
+
doc = Nokogiri::XML::Document.new
|
80
|
+
handshake = Nokogiri::XML::Node.new("handshake", doc)
|
81
|
+
doc.add_child(handshake)
|
82
|
+
handshake.content = hash
|
83
|
+
handshake
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,139 @@
|
|
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 :tmp_directory do |d|
|
42
|
+
d.destination = "#{application_name}/tmp"
|
43
|
+
end
|
44
|
+
empty_directory :log_directory do |d|
|
45
|
+
d.destination = "#{application_name}/log"
|
46
|
+
end
|
47
|
+
empty_directory :pid_directory do |d|
|
48
|
+
d.destination = "#{application_name}/tmp/pids"
|
49
|
+
end
|
50
|
+
|
51
|
+
# And now add the critical files
|
52
|
+
file :boot_file do |f|
|
53
|
+
f.source = "#{source_root}/config/boot.rb"
|
54
|
+
f.destination = "#{application_name}/config/boot.rb"
|
55
|
+
end
|
56
|
+
file :config_file do |f|
|
57
|
+
f.source = "#{source_root}/config/config.yaml"
|
58
|
+
f.destination = "#{application_name}/config/config.yaml"
|
59
|
+
end
|
60
|
+
file :dependencies_file do |f|
|
61
|
+
f.source = "#{source_root}/config/dependencies.rb"
|
62
|
+
f.destination = "#{application_name}/config/dependencies.rb"
|
63
|
+
end
|
64
|
+
file :dependencies_file do |f|
|
65
|
+
f.source = "#{source_root}/config/routes.rb"
|
66
|
+
f.destination = "#{application_name}/config/routes.rb"
|
67
|
+
end
|
68
|
+
template :component_file do |f|
|
69
|
+
f.source = "#{source_root}/script/component"
|
70
|
+
f.destination = "#{application_name}/script/component"
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Generates a new controller, with the corresponding stanzas and routes.
|
77
|
+
class ControllerGenerator < Templater::Generator
|
78
|
+
desc <<-DESC
|
79
|
+
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>],[...]"
|
80
|
+
DESC
|
81
|
+
|
82
|
+
first_argument :controller_name, :required => true, :desc => "Name of the Controller."
|
83
|
+
second_argument :actions_arg, :required => true, :as => :array, :default => [], :desc => "Actions implemented by this controller. Use the following syntax : name:priority:xpath"
|
84
|
+
|
85
|
+
def self.source_root
|
86
|
+
File.join(File.dirname(__FILE__), '../../templates/skates/app/controllers')
|
87
|
+
end
|
88
|
+
|
89
|
+
def controller_actions
|
90
|
+
@controller_actions ||= actions_arg.map { |a| a.split(":") }
|
91
|
+
end
|
92
|
+
|
93
|
+
def controller_class_name
|
94
|
+
"#{controller_name.capitalize}Controller"
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# This is a hack since Templater doesn't offer any simple way to edit files right now...
|
99
|
+
def add_route_for_actions_in_controller(actions, controller)
|
100
|
+
sentinel = "Skates.router.draw do"
|
101
|
+
router_path = "config/routes.rb"
|
102
|
+
actions.each do |action|
|
103
|
+
to_inject = "xpath(\"#{action[2]}\").to(:controller => \"#{controller}\", :action => \"#{action[0]}\").priority(#{action[1]})"
|
104
|
+
if File.exist?(router_path)
|
105
|
+
content = File.read(router_path).gsub(/(#{Regexp.escape(sentinel)})/mi){|match| "#{match}\n\t#{to_inject}"}
|
106
|
+
File.open(router_path, 'wb') { |file| file.write(content) }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
template :controller do |t|
|
112
|
+
t.source = "#{source_root}/controller.rb"
|
113
|
+
t.destination = "app/controllers/#{controller_name}_controller.rb"
|
114
|
+
self.add_route_for_actions_in_controller(controller_actions, controller_name)
|
115
|
+
# This is a hack since Templater doesn't offer any simple way to write several files from one...
|
116
|
+
FileUtils.mkdir("app/views/#{controller_name}") unless File.exists?("app/views/#{controller_name}")
|
117
|
+
controller_actions.each do |action|
|
118
|
+
FileUtils.cp("#{source_root}/../views/view.rb", "app/views/#{controller_name}/#{action[0]}.xml.builder")
|
119
|
+
end
|
120
|
+
|
121
|
+
# And now, let's create the stanza files
|
122
|
+
controller_actions.each do |action|
|
123
|
+
FileUtils.cp("#{source_root}/../stanzas/stanza.rb", "app/stanzas/#{action[0]}.rb")
|
124
|
+
# We need to replace
|
125
|
+
# "class Stanza < Skates::Base::Stanza" with "class #{action[0]} < Skates::Base::Stanza"
|
126
|
+
content = File.read("app/stanzas/#{action[0]}.rb").gsub("class Stanza < Skates::Base::Stanza", "class #{action[0].capitalize} < Skates::Base::Stanza")
|
127
|
+
File.open("app/stanzas/#{action[0]}.rb", 'wb') { |file| file.write(content) }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# The generators are added to the manifold, and assigned the names 'wiki' and 'blog'.
|
133
|
+
# So you can call them <script name> blog merb-blog-in-10-minutes and
|
134
|
+
# <script name> blog merb-wiki-in-10-minutes, respectively
|
135
|
+
add :application, ApplicationGenerator
|
136
|
+
add :controller, ControllerGenerator
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|